Skip to content
This repository was archived by the owner on Jul 15, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion .nanvix/z.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,18 @@
"echo-c",
"echo-cpp",
"file-c",
"fork-exec-c",
"fork-pid-c",
"fork-pthread-c",
"hello-c",
"hello-cpp",
"memory-c",
"misc-c",
"network-c",
"noop-c",
"noop-cpp",
"pipe-dup2-c",
"socket-fork-c",
"thread-c",
]

Expand All @@ -98,22 +103,49 @@
"thread-c",
]

# Suites that require ramfs-bundled shared libraries and only run in standalone mode.
# Reproducers for currently-OPEN Nanvix issues. These fail or hang BY DESIGN, so
# they are deliberately kept out of the gating integration run. They are still
# built (they appear in ALL_SUITES and src/Makefile) and smoke-tested (the build
# verifies they compile under -Wall -Wextra -Werror and that the binary exists).
# To observe a bug, run a reproducer manually by temporarily adding it to
# STANDALONE_ONLY_SUITES below (see README "Reproducers").
# fork-exec-c : vfsd filesystem I/O hangs after fork()+execv()
# pipe-dup2-c : dup2() of a pipe onto a standard stream (0/1/2) is refused
# socket-fork-c : a socket is shared (not reference-counted) across fork()
BUILD_ONLY_REPRODUCERS = [
"fork-exec-c",
"pipe-dup2-c",
"socket-fork-c",
]

# Build-only reproducers must still be real, built suites.
assert set(BUILD_ONLY_REPRODUCERS).issubset(ALL_SUITES)

# Suites that require ramfs-bundled shared libraries and/or only make sense in
# standalone mode (e.g. fork-based suites). fork-pid-c and fork-pthread-c assert
# POSIX fork() invariants (own pid in the child; re-init of inherited pthread
# primitives) that the pinned Nanvix version satisfies, so they run here as
# regression guards.
STANDALONE_ONLY_SUITES = [
"dlfcn-c",
"dlfcn-pie-c",
"fork-pid-c",
"fork-pthread-c",
]

# Suites that require host networking (passed as -allow-host-networking to nanvixd).
SUITES_REQUIRING_NETWORKING: set[str] = {
"network-c",
"socket-fork-c",
}

# Shared libraries that must be bundled into the ramfs for specific suites.
# Maps suite name to a list of (source_filename_in_build_dir, ramfs_target_path).
SUITE_RAMFS_LIBS: dict[str, list[tuple[str, str]]] = {
"dlfcn-c": [("libmul.so", "lib/libmul.so")],
"dlfcn-pie-c": [("libmul-pie.so", "lib/libmul-pie.so")],
# The execv() target read by the fork+exec reproducer, placed at "/target".
"fork-exec-c": [("fork-exec-target.elf", "target")],
}

# Docker image for cross-compilation.
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ PROCESS_MODE ?= multi-process
MEMORY_SIZE ?= 128mb

# 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

# ELF binaries produced by each suite.
BINARIES := $(addsuffix .elf,$(SUITES))
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,38 @@ as well as simple echo, hello-world, and no-op benchmarks.
| `echo-c` | Echo stdin to stdout (C) |
| `echo-cpp` | Echo stdin to stdout (C++) |
| `file-c` | File system operations (open, read, write, stat, link, mkdir, etc.) |
| `fork-exec-c` | fork()+execv() reproducer: an exec'd image hangs on its first vfsd file I/O |
| `fork-pid-c` | fork() test: asserts the child sees its own (new) pid from getpid()/getppid() |
| `fork-pthread-c` | fork() test: asserts the child can re-init an inherited pthread mutex/cond |
| `hello-c` | Hello world (C) |
| `hello-cpp` | Hello world (C++) |
| `memory-c` | malloc/free, aligned\_alloc, realloc, mmap/munmap, heap stress |
| `misc-c` | UID/GID, clock/time, uname, hostname, nanosleep, getenv |
| `network-c` | IPv4 (INET) and Unix domain sockets |
| `noop-c` | No-op program (C) |
| `noop-cpp` | No-op program (C++) |
| `pipe-dup2-c` | dup2() reproducer: dup2(pipe, STDOUT) does not redirect the standard stream |
| `socket-fork-c` | fork() reproducer: a child close()ing an inherited socket destroys the parent's socket |
| `thread-c` | Threading, mutexes, condition variables, rwlocks, TLS, TDA |

### Reproducers

Some suites are *reproducers* tied to specific Nanvix issues rather than
general conformance tests:

- `fork-pid-c` and `fork-pthread-c` assert POSIX `fork()` invariants (a child
sees its own pid; a child can re-initialize inherited pthread primitives).
The pinned Nanvix version satisfies these, so they run as **regression
guards** in the standalone integration run.
- `fork-exec-c`, `pipe-dup2-c`, and `socket-fork-c` reproduce **currently-open**
issues — they fail or hang by design (an exec'd image hanging on its first
vfsd I/O; `dup2()` of a pipe onto a standard stream being refused; a socket
being shared rather than reference-counted across `fork()`). To keep CI green
they are **built and smoke-tested but excluded from the gating integration
run** (`BUILD_ONLY_REPRODUCERS` in `.nanvix/z.py`). Each program's header
comment documents the expected (buggy) output. To observe a bug, temporarily
add the suite to `STANDALONE_ONLY_SUITES` and run `./z test`.

## Prerequisites

- [Docker](https://docs.docker.com/engine/install/)
Expand Down
2 changes: 1 addition & 1 deletion src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export LIBRARIES_DIR ?= $(BINARIES_DIR)
# Test Suites
#===============================================================================

SUITES := c-bindings dlfcn-c dlfcn-global-c dlfcn-needed-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-global-c dlfcn-needed-c dlfcn-pie-c echo-c echo-cpp file-c fork-exec-c fork-pid-c fork-pthread-c hello-c hello-cpp memory-c misc-c network-c noop-c noop-cpp pipe-dup2-c socket-fork-c thread-c

#===============================================================================
# Build Rules
Expand Down
27 changes: 27 additions & 0 deletions src/fork-exec-c/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright(c) The Maintainers of Nanvix.
# Licensed under the MIT License.

PROGRAM_NAME := fork-exec-c
BINARY := $(PROGRAM_NAME).elf

# The execv() target, bundled into the ramfs at "/target" by the harness
# (see .nanvix/z.py SUITE_RAMFS_LIBS).
TARGET_BINARY := fork-exec-target.elf

all: $(BINARIES_DIR)/$(BINARY) $(BINARIES_DIR)/$(TARGET_BINARY)

# Caller / init program (forks, then execs the target).
$(BINARIES_DIR)/$(BINARY): main.o
$(CC) $(LDFLAGS) main.o $(LIBRARIES) -o $@

# execv() target program (does the vfsd read that hangs after fork+exec).
$(BINARIES_DIR)/$(TARGET_BINARY): target.o
$(CC) $(LDFLAGS) target.o $(LIBRARIES) -o $@

clean:
rm -f main.o target.o
rm -f $(BINARIES_DIR)/$(BINARY)
rm -f $(BINARIES_DIR)/$(TARGET_BINARY)

%.o: %.c
$(CC) $(CFLAGS) $< -c -o $@
120 changes: 120 additions & 0 deletions src/fork-exec-c/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright(c) The Maintainers of Nanvix.
* Licensed under the MIT License.
*/

/*
* Reproducer: a fork()+execv()'d process hangs on its first vfsd filesystem
* I/O operation.
*
* Background
* ----------
* On Nanvix, execv() replaces the calling process's image and gives it a NEW
* main-thread identifier (the old thread is retired). Filesystem reads/writes
* are served by the vfsd daemon through a kernel push/pull rendezvous that is
* keyed by the client's (pid, tid). After fork()+execv(), the exec'd image's
* first vfsd request never completes -- the client blocks forever -- which
* makes any "fork then exec a program that touches the filesystem" workload
* (e.g. a Python subprocess, or `python script.py` spawned from a server) hang.
*
* What this program demonstrates (run in standalone mode)
* -------------------------------------------------------
* CALLER-START - the init/caller process starts.
* PARENT-READ-OK - the caller reads /target via vfsd successfully. This is
* the SAME vfsd read the target performs, proving vfsd I/O
* works from an ordinary (non-fork+exec) process.
* TARGET-STARTED - the child's execv("/target") succeeded and the new image
* is running (so fork() and execv() themselves work).
* TARGET-READ-OK - the exec'd image completed its vfsd read. <-- expected
* on a correct system; NEVER printed on the buggy system.
* ok - the whole sequence succeeded.
*
* Correct behavior : all markers print and the process exits 0.
* Buggy behavior : prints up to TARGET-STARTED, then hangs forever in the
* target's vfsd read (the run times out).
*
* The target program is bundled into the ramfs at "/target" by the test
* harness (see .nanvix/z.py SUITE_RAMFS_LIBS).
*/

#include <fcntl.h>
#include <sys/wait.h>
#include <unistd.h>

#define TARGET_PATH "/target"

static void emit(const char *s)
{
const char *p = s;
while (*p) {
p++;
}
(void)write(STDOUT_FILENO, s, (size_t)(p - s));
}

/* Reads the first 4 bytes of /target via vfsd and checks the ELF magic.
Returns 0 on success, -1 on failure. */
static int read_target_magic(void)
{
int fd = open(TARGET_PATH, O_RDONLY);
if (fd < 0) {
return (-1);
}
char buf[4];
ssize_t n = read(fd, buf, sizeof(buf));
(void)close(fd);
if (n != (ssize_t)sizeof(buf) || buf[0] != 0x7f || buf[1] != 'E' ||
buf[2] != 'L' || buf[3] != 'F') {
return (-1);
}
return (0);
}

int main(int argc, char *argv[])
{
(void)argc;
(void)argv;

emit("CALLER-START\n");

/* Control: the same vfsd read, but from this ordinary process. This works,
establishing that vfsd I/O is fine outside the fork+exec path. */
if (read_target_magic() != 0) {
emit("PARENT-READ-FAILED\n");
return (1);
}
emit("PARENT-READ-OK\n");

/* Now the failing case: fork, then exec a program that does the same read. */
emit("CALLER-FORKING\n");
pid_t pid = fork();
if (pid < 0) {
emit("FORK-FAILED\n");
return (1);
}

if (pid == 0) {
/* Child: replace image with the target. On success this never returns. */
char *const child_argv[] = {(char *)"target", (char *)0};
execv(TARGET_PATH, child_argv);
/* Only reached if execv() failed. */
emit("EXECV-FAILED\n");
_exit(127);
}

/* Parent: wait for the exec'd child. On the buggy system this blocks
forever because the child is stuck in its vfsd read. */
int status = 0;
pid_t w = waitpid(pid, &status, 0);
if (w != pid) {
emit("WAITPID-FAILED\n");
return (1);
}
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
emit("CHILD-NONZERO-EXIT\n");
return (1);
}

emit("ok\n");
return (0);
}
66 changes: 66 additions & 0 deletions src/fork-exec-c/target.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright(c) The Maintainers of Nanvix.
* Licensed under the MIT License.
*/

/*
* execv() target for the fork+exec vfsd-I/O reproducer.
*
* This program is loaded into the guest ramfs at "/target" and is execv()'d by
* the caller (fork-exec-c.elf). After exec, it does two things:
*
* 1. Writes a marker to stdout (kernel IKC path -- no vfsd involvement). This
* proves the exec'd image is actually running.
* 2. Performs a filesystem read via vfsd: it opens and reads its own ELF file
* at "/target" (guaranteed present in the ramfs). This is the operation
* that hangs when the process was reached via fork()+execv().
*
* On a correct system it prints both markers and exits 0. On the buggy system
* it prints only the first marker and then hangs forever inside the vfsd read.
*/

#include <fcntl.h>
#include <unistd.h>

#define SELF_PATH "/target"

static void emit(const char *s)
{
/* Write directly to stdout (fd 1). On Nanvix this is routed to the kernel
via IKC and does NOT go through vfsd, so it works in an exec'd image. */
const char *p = s;
while (*p) {
p++;
}
(void)write(STDOUT_FILENO, s, (size_t)(p - s));
}

int main(int argc, char *argv[])
{
(void)argc;
(void)argv;

emit("TARGET-STARTED\n");

/* The vfsd filesystem read that hangs after fork()+execv(). */
int fd = open(SELF_PATH, O_RDONLY);
if (fd < 0) {
emit("TARGET-OPEN-FAILED\n");
return (1);
}

char buf[4];
ssize_t n = read(fd, buf, sizeof(buf));
(void)close(fd);

if (n != (ssize_t)sizeof(buf) || buf[0] != 0x7f || buf[1] != 'E' ||
buf[2] != 'L' || buf[3] != 'F') {
emit("TARGET-READ-FAILED\n");
return (1);
}

/* Only reached if the vfsd read returned. */
emit("TARGET-READ-OK\n");
emit("ok\n");
return (0);
}
18 changes: 18 additions & 0 deletions src/fork-pid-c/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright(c) The Maintainers of Nanvix.
# Licensed under the MIT License.

PROGRAM_NAME := fork-pid-c

SOURCES := $(wildcard *.c)
OBJECTS := $(SOURCES:.c=.o)
BINARY := $(PROGRAM_NAME).elf

all: $(OBJECTS)
$(CC) $(LDFLAGS) $(OBJECTS) $(LIBRARIES) -o $(BINARIES_DIR)/$(BINARY)

clean:
rm -f $(OBJECTS)
rm -f $(BINARIES_DIR)/$(BINARY)

%.o: %.c
$(CC) $(CFLAGS) $< -c -o $@
Loading
Loading