help debugging flaky posix/tst-waitid.c test
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Fri Feb 14 11:46:05 GMT 2025
On 13/02/25 19:11, Aurelien Jarno wrote:
> On 2025-02-12 09:33, Adhemerval Zanella Netto wrote:
>>
>>
>> On 08/02/25 12:46, Aurelien Jarno wrote:
>>> Hi,
>>>
>>> The posix/tst-waitid.c appears to be flaky, although with a low
>>> probability to fail, but that's bad for continuous integration. Under
>>> heavy load, it crashes around once over 80000 times in my tests, but
>>> there is a way to increase the probability (see below). This happens at
>>> least on aarch64, riscv64 and x86-64, so it does not seems architecture
>>> specific at a first glance.
>>>
>>> When it happens, it fails with:
>>>
>>> tst-waitid.c:73: numeric comparison failure
>>> left: 0 (0x0); from: siginfo.si_status
>>> right: 19 (0x13); from: status
>>> error: 1 test failures
>>>
>>> This happens for the test on lines 156-173, which basically:
>>> - send a SIGSTOP to the child
>>> - wait for the child to appear in "T" state in /proc/$pid/status
>>> - call waitid with WEXITED|WSTOPPED on the child
>>> - call sigwaitinfo on the child
>>>
>>> In that later call, the siginfo.si_status field on the child ends-up
>>> with 0 instead of SIGSTOP.
>>>
>>> I have done some more tests on a separate code and it seems that this
>>> happens when kill, waitid and sigwaitinfo are called very quickly after
>>> each other. My guess is that this behaviour is explained by the
>>> following comment in that code:
>>>
>>> /* Wait the child stop. The waitid call below will block until it has
>>> stopped, but if we are real quick and enter the waitid system call
>>> before the SIGCHLD has been generated, then it will be discarded and
>>> never delivered. */
>>>
>>> It seems that the call to support_process_state_wait (pid, stop_state)
>>> should prevent that, but I wonder if there is a small race condition
>>> left. What I observed is that the 10ms nanosleep in
>>> support_process_state_wait between check of /proc/$pid/status makes the
>>> issue less probable. In other word, the following patch increases the
>>> chances for the tst-waitid test to fail:
>>>
>>> --- a/support/support_process_state.c
>>> +++ b/support/support_process_state.c
>>> @@ -80,9 +80,6 @@ support_process_state_wait (pid_t pid, enum support_process_state state)
>>>
>>> rewind (fstatus);
>>> fflush (fstatus);
>>> -
>>> - if (nanosleep (&(struct timespec) { 0, 10000000 }, NULL) != 0)
>>> - FAIL_EXIT1 ("nanosleep: %m");
>>> }
>>>
>>> free (line);
>>>
>>> Of course using strace here just hides the issue. Any idea how to debug
>>> or fix the issue?
>>
>> It seems that we can't really rely on siginfo_t::si_status being SIGSTOP
>> after a waitid call that returns info.si_status equal to SIGSTOP. Skimming
>> through kernel code, it seems that for CLD_STOPPED the si_status is obtained
>> from tsk->signal->group_exit_code (kernel/signal.c - do_notify_parent_cldstop),
>> but I can't really tell why on how it can be zero in this scenario.
>>
>> The tools/testing/selftests/pidfd/pidfd_wait.c also checks CLD_STOPPED,
>> but it does not check si_status. I think we should ignore it for now.
>
> Thanks for your feedback and for looking at that.
>
> I have done further test, and from them and from the comment in
> tst-waitid.c, siginfo_t::si_status ends up being 0 if waitid() is called
> before SIGCHLD has been generated.
>
> The test is waiting for the child to switch to CLD_STOPPED, but it seems
> the SIGCHLD is generated a tiny bit after. So there is a tiny window of
> time where the child is stopped but calling waitid followed by
> sigwaitinfo will return siginfo_t::si_status as 0.
>
> In that regard the following patch is enough to fix the issue for me and
> reliably get siginfo_t::si_status being SIGSTOP (tested on 10 millions
> executions):
>
> --- a/posix/tst-waitid.c
> +++ b/posix/tst-waitid.c
> @@ -162,6 +162,7 @@ do_test_waitd_common (idtype_t type, pid_t pid)
> before the SIGCHLD has been generated, then it will be discarded and
> never delivered. */
> support_process_state_wait (pid, stop_state);
> + nanosleep (&(struct timespec) { 0, 10000000 }, NULL);
>
> fail = waitid (type, pid, &info, WEXITED|WSTOPPED);
> TEST_COMPARE (fail, 0);
>
> However using a fixed delay in a test is never satisfactory and maybe
> the best is to stop checking that info.si_status equal to SIGSTOP.
>
I think a better way would to just remove the si_status check if we can not
guarantee the required synchronization. The sleep kind of work around
sometimes require us to reevaluate when new chips and/or kernel version.
More information about the Libc-alpha
mailing list