Skip to content
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
mainfrom
repro/fork-exec-vfsd-hang
Draft

[tests] Add fork/exec/dup2/socket POSIX reproducers#181
esaurez wants to merge 4 commits into
mainfrom
repro/fork-exec-vfsd-hang

Conversation

@esaurez

@esaurez esaurez commented Jun 19, 2026

Copy link
Copy Markdown

Summary

Adds five fork()/exec()/socket/dup2() test suites to src/, written while
enabling fork()+exec() for CPython on Nanvix. Each suite isolates one
specific POSIX behavior in a minimal, self-contained C program with
marker-based output (ok on success, a named marker + non-zero exit on
failure). Two are regression guards (currently pass); three reproduce
currently-open issues.

Suite What it checks On pinned Nanvix (0.16.32)
fork-pid-c a fork()'d child sees its own pid from getpid() and the parent's pid from getppid() passes (regression guard)
fork-pthread-c a fork()'d child can re-initialize an inherited pthread mutex/cond passes (regression guard)
fork-exec-c a fork()+execv()'d image's first vfsd filesystem I/O hangs (open issue)
pipe-dup2-c dup2(pipe, STDOUT_FILENO) redirects a standard stream fails (open issue)
socket-fork-c a socket is reference-counted across fork() (child close() must not kill the parent's socket) fails (open issue)

The behaviors, in detail

  • fork-pid-c — On Nanvix getpid() is memoized in a per-process cache that
    a child inherits copy-on-write. POSIX requires the child to observe its own
    new pid; a stale cache also breaks capability-checked calls (mmap during
    execv) with EACCES. Relates to CPython issue
    nanvix/cpython#471.
  • fork-pthread-c — A child must be able to rebuild its synchronization
    primitives after fork() (e.g. a fresh GIL in PyOS_AfterFork_Child); this
    requires pthread_mutex_init/pthread_cond_init to be idempotent at an
    address still present in the inherited registry. Relates to CPython issue
    nanvix/cpython#371.
  • fork-exec-cexecv() gives the exec'd image a new main-thread tid; the
    vfsd read/write rendezvous is keyed by (pid, tid), so the exec'd image's
    first 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 that fork()+execv() themselves work
    (TARGET-STARTED) before the read hangs.
  • pipe-dup2-c — Nanvix partitions the fd space by subsystem (stdio via
    kernel IKC; vfsd pipes at 1024+; sockets at 2048+), so dup2() cannot move a
    pipe fd onto fd 0/1/2. This breaks the classic pipe + dup2 + exec stdio
    redirection pattern. The probe is non-blocking (sets the read end
    O_NONBLOCK) so it never hangs.
  • socket-fork-cvfsd clones a process's open files on fork() (it has a
    ForkClone handler), but networkd maps a guest socket fd to a host socket
    by a fixed arithmetic offset with no per-process reference count, so parent
    and child share one underlying socket and the first close() in either tears
    it down for both. Reproduces
    nanvix/nanvix#2609.

CI behavior

fork-pid-c and fork-pthread-c assert invariants the pinned Nanvix version
satisfies, 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 are
therefore listed in BUILD_ONLY_REPRODUCERS in .nanvix/z.py: still compiled
(under -Wall -Wextra -Werror) and smoke-tested (binary exists), but excluded
from the gating integration run. To observe a bug, temporarily add the suite to
STANDALONE_ONLY_SUITES and run ./z test. The distinction is documented under
"Reproducers" in the README.

Wiring

  • src/<suite>/{main.c,Makefile} for each suite (plus target.c for
    fork-exec-c).
  • src/Makefile SUITES, .nanvix/z.py ALL_SUITES, and the appropriate
    run/category lists (STANDALONE_ONLY_SUITES, SUITES_REQUIRING_NETWORKING
    for socket-fork-c, BUILD_ONLY_REPRODUCERS).
  • README.md suite table + a "Reproducers" section.

Validation

  • ./z build builds all 25 binaries clean.
  • ./z test (standalone, Nanvix 0.16.32) is green: all smoke checks pass, the
    TESTABLE + regression-guard suites run and pass, and the build-only
    reproducers are correctly excluded — *** POSIX tests PASSED ***.
  • Each reproducer was verified to exhibit its documented behavior when run
    manually (hang / DUP2-RET-FAILED / SOCKET-KILLED-BY-CHILD-CLOSE).
  • .nanvix/z.py passes black --check and pyright --strict (0 errors).

Enrique Saurez and others added 4 commits June 19, 2026 09:31
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>
Copilot AI review requested due to automatic review settings June 19, 2026 18:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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), plus fork-exec-c, pipe-dup2-c, socket-fork-c (build-only reproducers).
  • Updated suite registries/build wiring (src/Makefile, top-level Makefile, .nanvix/z.py) including ramfs bundling for the fork-exec-c exec target.
  • Updated README.md with 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.

Comment thread Makefile

# 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 thread src/pipe-dup2-c/main.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 thread src/fork-pid-c/main.c
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 thread src/socket-fork-c/main.c
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 */
};
Comment thread .nanvix/z.py
"fork-pid-c",
"fork-pthread-c",
]

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants