This repository was archived by the owner on Jun 24, 2026. It is now read-only.
[tests] Add fork/exec/dup2/socket POSIX reproducers#181
Draft
esaurez wants to merge 4 commits into
Draft
Conversation
Adds a standalone test suite 'fork-exec-c' that reproduces a Nanvix bug: a process reached via fork()+execv() hangs on its first vfsd filesystem I/O operation. execv() replaces the calling image and assigns it a new main-thread id; vfsd serves file I/O through a kernel push/pull rendezvous keyed by the client (pid, tid). After fork()+execv(), the exec'd image's first vfsd request never completes, so any 'fork then exec a program that touches the filesystem' workload (e.g. a Python subprocess) hangs. The suite bundles a target program into the ramfs at /target and runs in standalone mode. The caller reads /target via vfsd successfully (PARENT-READ-OK), then forks and execs the target. The exec'd target prints TARGET-STARTED (proving fork+exec works) and then hangs in its own vfsd read of /target; TARGET-READ-OK is never reached. On a fixed system all markers print and the suite exits 0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add three POSIX behavior tests covering issues found while enabling fork()+exec() for CPython on Nanvix: - fork-pid-c: asserts a fork()'d child observes its own (new) pid from getpid() and the parent's pid from getppid(). Guards against the stale cached-pid regression that made capability-sensitive calls (mmap during execv) fail with EACCES in the child. - fork-pthread-c: asserts a fork()'d child can re-initialize an inherited pthread mutex/cond, mirroring the post-fork lock rebuild that runtimes such as CPython perform (a fresh GIL). - pipe-dup2-c: reproduces that dup2(pipe, STDOUT_FILENO) does not redirect a standard stream, because Nanvix partitions the fd space by subsystem (stdio via kernel IKC vs vfsd pipes). dup2 onto fd 1 is refused, so the classic pipe+dup2+exec stdio redirection pattern cannot work. fork-pid-c and fork-pthread-c currently pass on the pinned sysroot and act as regression guards; pipe-dup2-c fails, demonstrating the open limitation. Wired into src/Makefile, .nanvix/z.py (ALL_SUITES + STANDALONE_ONLY_SUITES) and documented in README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add socket-fork-c, a reproducer for the socket-not-reference-counted across fork() issue (nanvix/nanvix#2609). A parent creates and binds an AF_INET socket, forks, and the child close()s its inherited copy of the socket. POSIX requires fork() to give the child an independent reference, so the child's close() must leave the parent's socket usable; the test probes this with getsockname() before and after. On Nanvix networkd keeps no per-process reference count (it maps a guest socket fd to a host socket by a fixed arithmetic offset and has no ForkClone handler), so the child's close() tears down the shared host socket and the parent's getsockname() then fails (SOCKET-KILLED-BY-CHILD-CLOSE). This contrasts with vfsd, which clones file descriptors on fork() and behaves per POSIX. Needs host networking, so the suite is added to SUITES_REQUIRING_NETWORKING (the harness passes -allow-host-networking) and to STANDALONE_ONLY_SUITES. Verified to fail on the pinned sysroot, demonstrating the open issue. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
fork-exec-c, pipe-dup2-c and socket-fork-c reproduce currently-open Nanvix issues, so they fail or hang by design. The CI caller runs the full './z test' in standalone mode, so leaving these in STANDALONE_ONLY_SUITES would turn the pipeline red. Move them to a documented BUILD_ONLY_REPRODUCERS list: they are still compiled (ALL_SUITES + src/Makefile, under -Wall -Wextra -Werror) and smoke-tested (binary exists), but excluded from the gating integration run. fork-pid-c and fork-pthread-c assert invariants the pinned Nanvix (0.16.32) satisfies, so they remain in STANDALONE_ONLY_SUITES as regression guards. Document the distinction in the README. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds five new minimal C test suites under src/ to reproduce/guard specific POSIX fork()/execv()/dup2()/socket behaviors on Nanvix, and wires them into the build/test harness (with documentation explaining which are regression guards vs. build-only reproducers).
Changes:
- Added new suites:
fork-pid-c,fork-pthread-c(regression guards), plusfork-exec-c,pipe-dup2-c,socket-fork-c(build-only reproducers). - Updated suite registries/build wiring (
src/Makefile, top-levelMakefile,.nanvix/z.py) including ramfs bundling for thefork-exec-cexec target. - Updated
README.mdwith suite list entries and a new “Reproducers” section describing CI behavior.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
Makefile |
Adds new suites to the host Docker build list. |
src/Makefile |
Registers the new suites for cross-build/smoke testing. |
.nanvix/z.py |
Registers suites; marks some as build-only reproducers; adds standalone/networking wiring and ramfs bundling for /target. |
README.md |
Documents new suites and explains reproducers vs. regression guards. |
src/fork-pid-c/Makefile |
Build rules for fork-pid-c. |
src/fork-pid-c/main.c |
Regression guard validating child PID/PPID correctness after fork(). |
src/fork-pthread-c/Makefile |
Build rules for fork-pthread-c. |
src/fork-pthread-c/main.c |
Regression guard validating re-init of inherited pthread primitives after fork(). |
src/fork-exec-c/Makefile |
Builds both the caller and the exec target ELF for the fork+exec reproducer. |
src/fork-exec-c/main.c |
Reproducer for vfsd I/O hang on first filesystem op after fork()+execv(). |
src/fork-exec-c/target.c |
Exec target that performs the vfsd-backed read expected to hang on buggy systems. |
src/pipe-dup2-c/Makefile |
Build rules for pipe-dup2-c. |
src/pipe-dup2-c/main.c |
Reproducer for dup2(pipe, STDOUT) redirection failure. |
src/socket-fork-c/Makefile |
Build rules for socket-fork-c. |
src/socket-fork-c/main.c |
Reproducer for incorrect socket lifetime semantics across fork() when child closes its copy. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| # Test suites to build. | ||
| SUITES := c-bindings dlfcn-c dlfcn-pie-c echo-c echo-cpp file-c hello-c hello-cpp memory-c misc-c network-c noop-c noop-cpp thread-c | ||
| SUITES := c-bindings dlfcn-c dlfcn-pie-c echo-c echo-cpp file-c fork-exec-c hello-c hello-cpp memory-c misc-c network-c noop-c noop-cpp thread-c |
Comment on lines
+113
to
+128
| /* Make the read end non-blocking so a failed redirect does not hang. */ | ||
| int flags = fcntl(p[0], F_GETFL, 0); | ||
| if (flags < 0 || fcntl(p[0], F_SETFL, flags | O_NONBLOCK) < 0) { | ||
| emit("FCNTL-FAILED\n"); | ||
| return (1); | ||
| } | ||
|
|
||
| char buf[64]; | ||
| ssize_t n = read(p[0], buf, sizeof(buf)); | ||
| if (n != (ssize_t)strlen(MARKER) || memcmp(buf, MARKER, (size_t)n) != 0) { | ||
| /* The bytes written to STDOUT_FILENO did not reach the pipe: dup2 did | ||
| not redirect the standard stream. */ | ||
| emit("DUP2-REDIRECT-FAILED\n"); | ||
| return (1); | ||
| } | ||
| emit("DUP2-REDIRECT-OK\n"); |
Comment on lines
+103
to
+116
| (void)close(p[1]); | ||
| struct child_report rep = {0, 0}; | ||
| ssize_t n = read(p[0], &rep, sizeof(rep)); | ||
| (void)close(p[0]); | ||
|
|
||
| int status = 0; | ||
| if (waitpid(pid, &status, 0) != pid) { | ||
| emit("WAITPID-FAILED\n"); | ||
| return (1); | ||
| } | ||
| if (n != (ssize_t)sizeof(rep)) { | ||
| emit("CHILD-REPORT-FAILED\n"); | ||
| return (1); | ||
| } |
Comment on lines
+88
to
+93
| struct sockaddr_in sa = { | ||
| .sin_len = sizeof(sa), | ||
| .sin_family = AF_INET, | ||
| .sin_port = htons(2609), | ||
| .sin_addr = {.s_addr = htonl(0x7f000001)}, /* 127.0.0.1 */ | ||
| }; |
| "fork-pid-c", | ||
| "fork-pthread-c", | ||
| ] | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds five
fork()/exec()/socket/dup2()test suites tosrc/, written whileenabling
fork()+exec()for CPython on Nanvix. Each suite isolates onespecific POSIX behavior in a minimal, self-contained C program with
marker-based output (
okon success, a named marker + non-zero exit onfailure). Two are regression guards (currently pass); three reproduce
currently-open issues.
fork-pid-cfork()'d child sees its own pid fromgetpid()and the parent's pid fromgetppid()fork-pthread-cfork()'d child can re-initialize an inheritedpthreadmutex/condfork-exec-cfork()+execv()'d image's first vfsd filesystem I/Opipe-dup2-cdup2(pipe, STDOUT_FILENO)redirects a standard streamsocket-fork-cfork()(childclose()must not kill the parent's socket)The behaviors, in detail
fork-pid-c— On Nanvixgetpid()is memoized in a per-process cache thata child inherits copy-on-write. POSIX requires the child to observe its own
new pid; a stale cache also breaks capability-checked calls (
mmapduringexecv) withEACCES. Relates to CPython issuenanvix/cpython#471.
fork-pthread-c— A child must be able to rebuild its synchronizationprimitives after
fork()(e.g. a fresh GIL inPyOS_AfterFork_Child); thisrequires
pthread_mutex_init/pthread_cond_initto be idempotent at anaddress still present in the inherited registry. Relates to CPython issue
nanvix/cpython#371.
fork-exec-c—execv()gives the exec'd image a new main-thread tid; thevfsd read/write rendezvous is keyed by
(pid, tid), so the exec'd image'sfirst filesystem request never rendezvous-matches and the process blocks
forever. The suite proves vfsd I/O works from an ordinary process
(
PARENT-READ-OK) and thatfork()+execv()themselves work(
TARGET-STARTED) before the read hangs.pipe-dup2-c— Nanvix partitions the fd space by subsystem (stdio viakernel IKC; vfsd pipes at 1024+; sockets at 2048+), so
dup2()cannot move apipe fd onto fd 0/1/2. This breaks the classic pipe +
dup2+execstdioredirection pattern. The probe is non-blocking (sets the read end
O_NONBLOCK) so it never hangs.socket-fork-c—vfsdclones a process's open files onfork()(it has aForkClonehandler), butnetworkdmaps a guest socket fd to a host socketby a fixed arithmetic offset with no per-process reference count, so parent
and child share one underlying socket and the first
close()in either tearsit down for both. Reproduces
nanvix/nanvix#2609.
CI behavior
fork-pid-candfork-pthread-cassert invariants the pinned Nanvix versionsatisfies, so they run in the standalone integration set as regression guards.
The three open-bug reproducers fail or hang by design, which would turn the
full
./z test(run in standalone mode by the CI caller) red. They aretherefore listed in
BUILD_ONLY_REPRODUCERSin.nanvix/z.py: still compiled(under
-Wall -Wextra -Werror) and smoke-tested (binary exists), but excludedfrom the gating integration run. To observe a bug, temporarily add the suite to
STANDALONE_ONLY_SUITESand run./z test. The distinction is documented under"Reproducers" in the README.
Wiring
src/<suite>/{main.c,Makefile}for each suite (plustarget.cforfork-exec-c).src/MakefileSUITES,.nanvix/z.pyALL_SUITES, and the appropriaterun/category lists (
STANDALONE_ONLY_SUITES,SUITES_REQUIRING_NETWORKINGfor
socket-fork-c,BUILD_ONLY_REPRODUCERS).README.mdsuite table + a "Reproducers" section.Validation
./z buildbuilds all 25 binaries clean../z test(standalone, Nanvix 0.16.32) is green: all smoke checks pass, theTESTABLE + regression-guard suites run and pass, and the build-only
reproducers are correctly excluded —
*** POSIX tests PASSED ***.manually (hang /
DUP2-RET-FAILED/SOCKET-KILLED-BY-CHILD-CLOSE)..nanvix/z.pypassesblack --checkandpyright --strict(0 errors).