-
-
Notifications
You must be signed in to change notification settings - Fork 645
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Let signals interrupt fgets unless SA_RESTART set #1152
Conversation
Thank you for the thorough investigation and fix. To ensure I'm not sure if waiving copyright/putting it in public domain is enough to merge this. If you don't have any objections could you assign copyright to Justine as described here: https://github.com/jart/cosmopolitan/blob/master/CONTRIBUTING.md#copyright-assignment ? Also please note it here so I can confirm. |
See investigation in jart#1130. fgets internally calls readv. readv is a @restartable function that understands SA_RESTART. If SA_RESTART is set, readv already handles restarting the system call and eventually the string is transparently returned to the fgets caller. When SA_RESTART is not set, -1 EINTR should bubble up to the fgets caller. However, until this commit, fgets itself would detect EINTR and keep retrying until it read an entire line. This commit fixes this behaviour so that fgets understands SA_RESTART. I hereby assign copyright for this commit to Justine Tunney. Signed-off-by: Cadence Ember <cadence@disroot.org>
The new test only passes thanks to the previous commit.
I have now done the copyright assignment process, and have checked in my test file with a copyright notice under my own name as encouraged. I haven't written C since university so I hope my test file is doing the right thing. I will welcome any suggestions. I did clang-format and valgrind, both tools are happy with my code. I also made sure that the test only passes when fgets_unlocked.c has the fix. If you undo the fix in fgets_unlocked.c, you'll see the test fail. I was not able to figure out how to run the entire test suite and couldn't find any instructions. The output of |
You can run the test suite just by making items under Unfortunately it looks like your test needs tweaks as it fails on my Linux system and on the CI runs above:
|
Looks like the |
I couldn't figure out what was causing the race condition. Setting sched_setaffinity to a single core is the only way I could find to fix it reliably. Sorry for resorting to an inelegant hack. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests look good overall, thank you! No worries about sched_setaffinity
, I'm not surprised something like it is needed to make this reproducible.
#include "libc/isystem/errno.h" | ||
#include "libc/isystem/sched.h" | ||
#include "libc/isystem/signal.h" | ||
#include "libc/isystem/stddef.h" | ||
#include "libc/isystem/unistd.h" | ||
#include "libc/stdio/stdio.h" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#include "libc/isystem/errno.h" | |
#include "libc/isystem/sched.h" | |
#include "libc/isystem/signal.h" | |
#include "libc/isystem/stddef.h" | |
#include "libc/isystem/unistd.h" | |
#include "libc/stdio/stdio.h" | |
#include <errno.h> | |
#include <sched.h> | |
#include <signal.h> | |
#include <stddef.h> | |
#include <stdio.h> | |
#include <unistd.h> |
Formatting: I don't see a precedent for including from isystem
in tests. Using either the direct paths (such as libc/stdio/stdio.h
) or using the standard include format <stdio.h>
is both allowed, I would just stay consistent within the file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See test/posix/...
now that normal includes are possible I'm more encouraging of them for c standard library headers.
void SetUpOnce(void) { | ||
cpu_set_t set; | ||
CPU_ZERO(&set); | ||
CPU_SET(1, &set); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CPU_SET(1, &set); | |
CPU_SET(0, &set); |
I had to set it 0 to run on my single core Windows 10 vm.
|
||
TEST(fgets_eintr, testThatFgetsReadsFromPipeNormally) { | ||
setup_signal_and_pipe(0); // 0 = no SA_RESTART | ||
ASSERT_NE(-1, (pid = fork())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider rather than using fork
, exit
, etc directly use SPAWN
, PARENT
, and WAIT
from #include "libc/testlib/subprocess.h"
as a standard way to fork
and wait on children hooked in with the test library. See test/libc/sock/recvfrom_test.c
for an example.
On testThatTheSignalInterruptsFgets
have the child EXPECT_SYS(0, 0, sigignore(SIGPIPE));
so that it exits successfully.
TEST(fgets_eintr, testThatTheSignalInterruptsFgets) { | ||
setup_signal_and_pipe(0); // 0 = no SA_RESTART | ||
ASSERT_NE(-1, (pid = fork())); | ||
if (!pid) { | ||
write_pipe(1); // 1 = signal | ||
_exit(0); | ||
} | ||
read_pipe(); | ||
EXPECT_STRNE(MY_TEST_STRING_1 MY_TEST_STRING_2, buf); | ||
EXPECT_EQ(EINTR, errno); | ||
EXPECT_EQ(1, got_sigusr1); | ||
} | ||
|
||
TEST(fgets_eintr, testThatFgetsRestartsWhenSaRestartIsSet) { | ||
setup_signal_and_pipe(SA_RESTART); // SA_RESTART | ||
ASSERT_NE(-1, (pid = fork())); | ||
if (!pid) { | ||
write_pipe(1); // 1 = signal | ||
_exit(0); | ||
} | ||
read_pipe(); | ||
EXPECT_STREQ(MY_TEST_STRING_1 MY_TEST_STRING_2, buf); | ||
EXPECT_NE(EINTR, errno); | ||
EXPECT_EQ(1, got_sigusr1); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately neither of these tests run successfully on Windows. However, I think it's unrelated to this work. #1160
I would #include "libc/dce.h"
and then at the start of these tests add something like:
// [Windows] process terminates when receiving signal during readv #1160
if (IsWindows())
return;
to bail out successfully noting why it's disabled on Windows.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Contributors are expected to make their best effort to write code that works across platforms, however they're only required to pass GitHub Actions to get merged. I'm happy to add if (IsWindows())
statements myself when I run this across my test fleet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approved for copyright and for the change itself. Thank you!
See investigation in #1130.
fgets internally calls readv. readv is a
@restartable
function that understands SA_RESTART. If SA_RESTART is set, readv already handles restarting the system call and eventually the string is transparently returned to the fgets caller.When SA_RESTART is not set, -1 EINTR should bubble up to the fgets caller. However, until this commit, fgets itself would detect EINTR and keep retrying until it read an entire line.
This commit fixes this behaviour so that fgets understands SA_RESTART.