From 40b71851f5266c1c721583033f7a66fc588c97d3 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 9 Sep 2026 20:05:22 +0200 Subject: [PATCH] feat: extend user and waitanyfast --- user/user.ld | 28 +++++++++++++++++++++++----- user/waitanyfast.c | 18 ++++++++++++++++++ user/waitanyslow.c | 23 +++++++++++++++++++++++ 3 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 user/waitanyfast.c create mode 100644 user/waitanyslow.c diff --git a/user/user.ld b/user/user.ld index 298a01d..8f2cf91 100644 --- a/user/user.ld +++ b/user/user.ld @@ -1,18 +1,36 @@ ENTRY(_start) +/* Two PT_LOAD segments, not one: text+rodata read+execute only (FLAGS(5) + = PF_R|PF_X), data+bss read+write only (FLAGS(6) = PF_R|PF_W) -- real + W^X for every binary this links, mirroring the kernel's own linker.ld + (:text/:rodata/:data). elf.c's elfLoad reads each segment's own p_flags + (elfSegmentPagingFlags) to map it with paging.h's pageWrite/pageNoExecute + accordingly, so a binary that writes into its own string literals or + jumps into its own .data now takes a real, catchable SIGSEGV instead of + silently succeeding the way one shared RWX segment always allowed -- + see user/elfwritevictim.c and user/elfexecvictim.c for the live proof. */ PHDRS { - load PT_LOAD FILEHDR PHDRS FLAGS(7); + text PT_LOAD FILEHDR PHDRS FLAGS(5); + data PT_LOAD FLAGS(6); } SECTIONS { . = USER_BASE + SIZEOF_HEADERS; - .text : { *(.text .text.*) } :load - .rodata : { *(.rodata .rodata.*) } :load - .data : { *(.data .data.*) } :load - .bss : { *(.bss .bss.*) *(COMMON) } :load + .text : { *(.text .text.*) } :text + .rodata : { *(.rodata .rodata.*) } :text + + /* Fresh page for .data: the two PT_LOAD segments above must never + share a page, or the CPU would have to apply the union of both + segments' flags to that shared page, quietly reintroducing a + writable+executable (or writable .rodata) page exactly where this + split exists to remove one. */ + . = ALIGN(4096); + + .data : { *(.data .data.*) } :data + .bss : { *(.bss .bss.*) *(COMMON) } :data /DISCARD/ : { diff --git a/user/waitanyfast.c b/user/waitanyfast.c new file mode 100644 index 0000000..87dd9cf --- /dev/null +++ b/user/waitanyfast.c @@ -0,0 +1,18 @@ +#include "ulib.h" + +/* + * Only ever spawned by waitanytest.c. Exits immediately with no sleep at + * all, so waitanytest.c's very first userWaitAny() call is overwhelmingly + * likely to find this one already Dead (the other child, waitanyslow.c, + * cannot possibly have finished its own userSleep by then) -- proves + * threadJoinAnyChild's waitScanFoundDead path picks up an already-dead + * child without ever needing to block at all. + */ +int main(void) +{ + print("waitanyfast: start, pid="); + printDec(userGetpid()); + print("\n"); + print("waitanyfast: exiting now\n"); + return 44; +} diff --git a/user/waitanyslow.c b/user/waitanyslow.c new file mode 100644 index 0000000..f449664 --- /dev/null +++ b/user/waitanyslow.c @@ -0,0 +1,23 @@ +#include "ulib.h" + +/* + * Only ever spawned by waitanytest.c. Sleeps a real, non-trivial amount + * before exiting -- long enough that waitanytest.c's own userWaitAny() + * calls cannot possibly race past it by luck: its first call happens + * essentially immediately after spawning both children (see + * waitanytest.c's own file comment), so this thread genuinely cannot be + * Dead yet by the time that first call runs. That is what forces + * waitanytest.c's SECOND userWaitAny() call to take threadJoinAnyChild's + * waitScanNoneDead branch and actually block through the real + * retry-syscall path, rather than getting lucky and finding it already + * dead too. + */ +int main(void) +{ + print("waitanyslow: start, pid="); + printDec(userGetpid()); + print("\n"); + userSleep(150); + print("waitanyslow: exiting now\n"); + return 33; +}