From 43089a9a7c38c530bdd8fa10258e5dd8ea3490d9 Mon Sep 17 00:00:00 2001 From: Sami Jawhar Date: Mon, 26 Oct 2026 17:32:08 +0000 Subject: [PATCH 1/2] daemon_test: the long-line check counts the empty end of a 2 MiB line as a piece The sabotage arm "the end of a worker line too long to relay is sent as a line" survived 9 of 60 runs of a_worker_line_too_long_to_relay_is_dropped_whole. The runs were in the fedora:44 CI image on a loaded host. With the test pinned to one CPU, the arm survived 12 of 30. The relay's buffer stops doubling at 2 MiB, the length of the test's longest line. A read that fills it with that line stops just before the line's NUL. With `s->skip = 1` gone, the rest of that line reaches the controller as an empty line. The check counted only lines that start with "x". In every run the arm survived, the five shorter lines had their NUL in the read that passed LINE_LIMIT, and were dropped whole. The 2 MiB line's end arrived empty. The check now counts an empty line as a piece. A 2 MiB line never fits the buffer with its NUL, so its start is always dropped. With the arm, its end always reaches the controller, empty or not. The check caught the arm in 100 of 100 runs, and the test passed 100 of 100 without it. (cherry picked from commit 9f143eaf3b959414b9c2ad2a45860293afe2b58b) --- tests/daemon_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/daemon_test.py b/tests/daemon_test.py index 83cd81d..6ff8c8a 100644 --- a/tests/daemon_test.py +++ b/tests/daemon_test.py @@ -995,7 +995,8 @@ def a_worker_line_too_long_to_relay_is_dropped_whole(): check(d.wait_event("remote_pages_changed ", since=mark) == "remote_pages_changed 3", "the line after the %d-byte one, unchanged" % size) with d.lock: - pieces = ["%d bytes" % len(e) for e in d.events[mark:] if e.startswith("x")] + # an empty line is a piece too: the end of a 2 MiB line whose start filled the relay buffer + pieces = ["%d bytes" % len(e) for e in d.events[mark:] if e.startswith("x") or not e] check(not pieces, "no piece of a %d-byte line reaches the controller: %s" % (size, pieces)) # the longest line relayed arrives whole mark = d.mark() From e3633eb494f9b5dcadcae408d6730caaf4ac65c7 Mon Sep 17 00:00:00 2001 From: Sami Jawhar Date: Mon, 26 Oct 2026 17:32:08 +0000 Subject: [PATCH 2/2] tests: the fork fault returns once the process it leaves behind has written its pid (#113) The two "left in its own session" tests of test-probe failed 10 of 60 runs (30 of each). The runs were in the fedora:44 CI image on a loaded host. They failed with "the stress plugin did not leave its process" or with the ValueError of #113. The fault forked and returned at once. In 100 runs by the tests' own run() and run_lv2(), each run took 10 to 90 ms. In 36, the pidfile was missing (18) or empty (18). The runner can kill the process left behind before that process writes its pid. In all 100 runs, the probe's document says that the process left behind was killed. No process with that pidfile in its environment was alive then or a second later. The runner does its job. The fault returned too soon. Writing the pid by rename, as #113 proposes, would leave the file missing instead. The test cannot tell a missing file from a fault that never forked. In both fixtures, the fault now waits on a pipe. The process left behind closes the pipe once it is in its own session and its pid is written. With a 200 ms sleep added after its setsid(), both tests fail 10 of 10 runs each before this change. After it, they pass 10 of 10 each. Without the sleep, they passed 120 of 120 runs (60 of each). (cherry picked from commit 4ec32d40bfafc35b0672bdbd40df82ecfc9ee17d) --- tests/lv2_fixture.c | 34 ++++++++++++++++++++++------------ tests/stress_plugin.c | 11 +++++++++-- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/tests/lv2_fixture.c b/tests/lv2_fixture.c index 934fdf1..e275fb6 100644 --- a/tests/lv2_fixture.c +++ b/tests/lv2_fixture.c @@ -107,11 +107,13 @@ static void run(LV2_Handle instance, uint32_t frames) /* stress: the gain, with the fault STRESS_FAULT names on its first run() in the process, as tests/stress_plugin.c does * for CLAP: crash aborts, hang spins forever, fork leaves a process of its own session behind (its pid written to the - * file STRESS_FORK_PIDFILE names) */ + * file STRESS_FORK_PIDFILE names before run() returns) */ static void fault_once(void) { static int done; const char *fault = getenv("STRESS_FAULT"); + int ready[2]; + char c; if (done || !fault) return; @@ -121,23 +123,31 @@ static void fault_once(void) if (!strcmp(fault, "hang")) for (;;) ; - if (!strcmp(fault, "fork") && fork() == 0) + if (!strcmp(fault, "fork") && pipe(ready) == 0) { - const char *pidfile = getenv("STRESS_FORK_PIDFILE"); - - setsid(); - if (pidfile) + if (fork() == 0) { - FILE *f = fopen(pidfile, "w"); + const char *pidfile = getenv("STRESS_FORK_PIDFILE"); - if (f) + setsid(); + if (pidfile) { - fprintf(f, "%d\n", (int)getpid()); - fclose(f); + FILE *f = fopen(pidfile, "w"); + + if (f) + { + fprintf(f, "%d\n", (int)getpid()); + fclose(f); + } } + close(ready[1]); + sleep(60); + _exit(0); } - sleep(60); - _exit(0); + close(ready[1]); + while (read(ready[0], &c, 1) > 0) + ; + close(ready[0]); } } diff --git a/tests/stress_plugin.c b/tests/stress_plugin.c index f58c225..0dce30a 100644 --- a/tests/stress_plugin.c +++ b/tests/stress_plugin.c @@ -23,7 +23,7 @@ * STRESS_FAULT=crash the first process call aborts * STRESS_FAULT=hang the first process call never returns * STRESS_FAULT=fork the first process call leaves a process behind that starts its own session and sleeps a - * minute; its pid is written to the file STRESS_FORK_PIDFILE names + * minute; the call returns once its pid is written to the file STRESS_FORK_PIDFILE names */ /* A stereo passthrough .clap the daemon's tests break on purpose: * org.plugin-hostd.test.stress parameters @@ -302,6 +302,8 @@ static void fault_once(void) { static int done; const char *fault = getenv("STRESS_FAULT"); + int ready[2]; + char c; if (done || !fault) return; @@ -311,7 +313,7 @@ static void fault_once(void) if (!strcmp(fault, "hang")) for (;;) ; - if (!strcmp(fault, "fork")) + if (!strcmp(fault, "fork") && pipe(ready) == 0) { pid_t pid = fork(); @@ -330,9 +332,14 @@ static void fault_once(void) fclose(f); } } + close(ready[1]); sleep(60); _exit(0); } + close(ready[1]); + while (read(ready[0], &c, 1) > 0) + ; + close(ready[0]); } }