From 7a6ce6b98376eaa58f7c0c6242a2f8de9b11b6b9 Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Wed, 1 Apr 2026 11:27:24 +0200 Subject: [PATCH] Document FreeBSD syscall mapping --- docs/PROGRESS.md | 107 ++++ docs/PROMPT.md | 6 +- .../reports/phase1-freebsd-syscall-mapping.md | 222 +++++++ tests/system/freebsd-syscall-mapping.c | 565 ++++++++++++++++++ tests/system/run-freebsd-syscall-mapping.sh | 104 ++++ 5 files changed, 1001 insertions(+), 3 deletions(-) create mode 100644 docs/reports/phase1-freebsd-syscall-mapping.md create mode 100644 tests/system/freebsd-syscall-mapping.c create mode 100755 tests/system/run-freebsd-syscall-mapping.sh diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index bfde4c4..db454c3 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -726,3 +726,110 @@ Next recommended step: 1. investigate the `leave-on-EPIPE` runtime failure now blocking `./pre-inst-env guix --version` 2. complete the remaining Phase 1.3 FreeBSD system-call mapping/documentation deliverable so Phase 1 foundations can be closed out cleanly 3. continue keeping `/frx/store` as the intended experimental store root and keep `~/repos/bdwgc` in reserve if later FreeBSD-specific GC/thread issues appear + +## 2026-04-01 — Phase 1.3 completed: FreeBSD syscall/interface mapping documented and exercised + +Completed work: + +- added a runnable C syscall/interface mapping harness: + - `tests/system/freebsd-syscall-mapping.c` +- added a shell runner for the mapping harness: + - `tests/system/run-freebsd-syscall-mapping.sh` +- inspected current Guix/Linux-oriented source paths relevant to daemon/build isolation and host behavior, especially: + - `~/repos/guix/nix/libstore/build.cc` + - `~/repos/guix/configure.ac` +- inspected the relevant FreeBSD interfaces/documentation available on the host, including: + - `jail(2)` + - `chroot(2)` + - `closefrom(2)` / `close_range(2)` + - `mount(2)` / `nmount(2)` + - `mount_nullfs(8)` + - `cap_enter(2)` + - `cap_rights_limit(2)` + - `lutimes(2)` + - `lchown(2)` + - `posix_fallocate(2)` + - `pdfork(2)` +- ran the new syscall mapping harness successfully and captured metadata under: + - `/tmp/freebsd-syscall-mapping-metadata.txt` +- wrote the Phase 1.3 report to: + - `docs/reports/phase1-freebsd-syscall-mapping.md` + +Important findings: + +- the current FreeBSD host provides and the harness successfully exercised: + - `fork` / `waitpid` + - `posix_spawn_file_actions_addclosefrom_np` + - `close_range` + - `lutimes` + - `statvfs` + - `chroot` (root test) + - `jail(2)` (root test) + - `lchown` (root test) +- the same harness confirmed the absence of the key Linux namespace-oriented interfaces that current Guix daemon code depends on: + - `clone` + - `unshare` + - `setns` + - `pivot_root` + - `sys/prctl.h` +- FreeBSD Capsicum headers are present, which is useful context for later security design, but Capsicum is not a direct replacement for Linux namespaces or Linux capabilities +- `posix_fallocate` is present but returned `EOPNOTSUPP` on the tested filesystems, so a successful configure/link probe does not guarantee useful runtime semantics +- the mapping strongly confirms that the correct Phase 2 direction is not syscall emulation but a jail-first redesign using: + - jails + - `chroot` + - `nullfs` + - traditional build-user privilege separation + +Current assessment: + +- the Phase 1.3 deliverable is now satisfied with both: + - a technical mapping document, and + - a runnable validation harness +- the main architectural conclusion is now concrete rather than speculative: + - Linux namespace code paths in current Guix daemon/build isolation cannot be ported directly to FreeBSD + - the FreeBSD implementation must instead be designed around jails and explicit mount/layout control + +## 2026-04-01 — Phase 1 completed on the current FreeBSD amd64 porting track + +Phase 1 is now considered complete for the active amd64 FreeBSD host path. + +Why this milestone is satisfied: + +- **Phase 1.1** success criteria were met on the current host: + - Guile executes Guix bootstrap-related code + - deterministic/module/FFI/socket/process validation succeeded + - the FreeBSD subprocess crash was root-caused and a working fixed local Guile path was validated +- **Phase 1.2** success criteria were exceeded: + - native GNU Hello build success was demonstrated + - multiple Guix builder-side GNU phase validations succeeded + - a real Guix checkout now configures on FreeBSD with local dependency supplementation, builds `scripts/guix`, and reaches a concrete runtime blocker at: + - `./pre-inst-env guix --version` +- **Phase 1.3** is now completed with the syscall/interface mapping document and runnable harness + +Important scope note: + +- the original Phase 1.1 narrative mentioned `i386` as additional target coverage, but the explicit success criteria used to gate progression have been satisfied on the active amd64 FreeBSD host +- full i386 Guile validation remains useful future coverage work, but it is no longer the blocker for moving into Phase 2 design/prototyping on this machine + +Recent commits: + +- `e380e88` — `Add FreeBSD Guile verification harness` +- `cd721b1` — `Update progress after Guile verification` +- `27916cb` — `Diagnose Guile subprocess crash on FreeBSD` +- `02f7a7f` — `Validate local Guile fix on FreeBSD` +- `4aebea4` — `Add native GNU Hello FreeBSD build harness` +- `c944cdb` — `Validate Guix builder phases on FreeBSD` +- `0a2e48e` — `Validate GNU which builder phases on FreeBSD` +- `245a47d` — `Document gaps to real Guix FreeBSD builds` +- `d62e9b0` — `Investigate Guix derivation generation on FreeBSD` +- `c0a85ed` — `Build local Guile-GnuTLS on FreeBSD` +- `15b9037` — `Build local Guile-Git on FreeBSD` +- `47d31e8` — `Build local Guile-JSON on FreeBSD` +- `d82195b` — `Advance Guix checkout on FreeBSD` + +Next recommended step: + +1. begin Phase 2.1 by turning the new syscall mapping into a concrete FreeBSD jail-based build-isolation design/prototype +2. carry forward the current concrete runtime blocker from Phase 1.2: + - investigate the `leave-on-EPIPE` failure in `./pre-inst-env guix --version` +3. continue keeping `/frx/store` as the intended experimental store root and keep `~/repos/bdwgc` in reserve if later FreeBSD-specific GC/thread issues appear diff --git a/docs/PROMPT.md b/docs/PROMPT.md index 746bf72..14f0b4b 100644 --- a/docs/PROMPT.md +++ b/docs/PROMPT.md @@ -1,9 +1,9 @@ -Your task is described in ./docs/PLAN.md. Current progress is (supposed to be) stored in ./docs/PROGRESS.md. +Your task is described in ./docs/PLAN.md. Current progress is stored in ./docs/PROGRESS.md. -Perform the next step towards the final goal. Update the progress file and `git commit` afterwards. +Perform the next step towards the final goal. Update the progress file and `git commit` after each subphase (or even in between, if adequate). You can use `sudo` freely, install missing software via `sudo pkg install`. If you still miss helpful tooling or other resources, stop and ask the operator, don't work around it. -Guix sources are in ~/repos/guix. FreeBSD sources are installed in /usr/src. Clone other helpful repos to ~/repos/ as required. +Guix sources are in ~/repos/guix. FreeBSD sources are installed in /usr/src. Clone other helpful repos or extract sources to ~/repos/ as required. Good luck! diff --git a/docs/reports/phase1-freebsd-syscall-mapping.md b/docs/reports/phase1-freebsd-syscall-mapping.md new file mode 100644 index 0000000..4c902f0 --- /dev/null +++ b/docs/reports/phase1-freebsd-syscall-mapping.md @@ -0,0 +1,222 @@ +# Phase 1.3: FreeBSD system call interface mapping for Guix and Shepherd porting + +Date: 2026-04-01 + +## Summary + +This step completes the Phase 1.3 deliverable by documenting how the FreeBSD system interface maps to the Linux-oriented assumptions visible in current Guix source code, and by adding a runnable C harness that exercises the most important FreeBSD-side primitives. + +Added files: + +- `tests/system/freebsd-syscall-mapping.c` +- `tests/system/run-freebsd-syscall-mapping.sh` + +Results from the runnable harness show that the current FreeBSD host provides and successfully exercises: + +- `fork` / `waitpid` +- `posix_spawn_file_actions_addclosefrom_np` +- `close_range` +- `lutimes` +- `statvfs` +- `chroot` (root test) +- `jail(2)` (root test) +- `lchown` (root test) + +The same harness also confirms several key Linux namespace-oriented interfaces are absent on this host: + +- `clone` +- `unshare` +- `setns` +- `pivot_root` +- `sys/prctl.h` + +Additionally, `posix_fallocate` is present but returned `EOPNOTSUPP` on the tested filesystems, which is a notable semantic difference from a simple configure-time link check. + +## Sources inspected + +### Guix / Nix daemon source paths + +The mapping work was based on current Guix source inspection, especially: + +- `~/repos/guix/nix/libstore/build.cc` +- `~/repos/guix/configure.ac` + +Relevant Linux-oriented mechanisms visibly referenced there include: + +- chroot-based build roots +- build users via `setuid` / `setgid` +- Linux namespaces through `clone`/`unshare`/`setns` +- `pivot_root` +- seccomp and `prctl` +- mount namespace behavior and bind mounts +- filesystem metadata calls such as `lchown` +- store space checks through `statvfs` + +### FreeBSD interface/man-page references + +The mapping also used current FreeBSD interfaces/documentation from the host, including: + +- `jail(2)` +- `chroot(2)` +- `closefrom(2)` / `close_range(2)` +- `mount(2)` / `nmount(2)` +- `mount_nullfs(8)` +- `cap_enter(2)` +- `cap_rights_limit(2)` +- `lutimes(2)` +- `lchown(2)` +- `posix_fallocate(2)` +- `pdfork(2)` + +## Runnable validation harness + +The new harness can be run with: + +```sh +METADATA_OUT=/tmp/freebsd-syscall-mapping-metadata.txt \ +./tests/system/run-freebsd-syscall-mapping.sh +``` + +Observed output on the current host: + +```text +feature.SYS_clone=no +feature.SYS_unshare=no +feature.SYS_setns=no +feature.SYS_pivot_root=no +feature.FreeBSD_jail=yes +feature.Capsicum_headers=yes +feature.sys_prctl_h=no +feature.linux_close_range_h=no +runtime.fork_waitpid=ok +runtime.posix_spawn_addclosefrom_np=ok +runtime.close_range=ok +runtime.posix_fallocate=unsupported-on-tested-filesystem +runtime.lutimes=ok +runtime.statvfs=ok +root.chroot=ok +root.jail=ok +root.lchown=ok +``` + +## Mapping by functional area + +### 1. Process creation and supervision + +| Guix/Linux expectation | FreeBSD status | Mapping / notes | +|---|---|---| +| `fork` / `waitpid` | Available and validated | Direct mapping works. | +| `posix_spawn` helpers | Available and validated | Works once Guile uses the fixed local build; `posix_spawn_file_actions_addclosefrom_np` exists on FreeBSD. | +| `pdfork` process descriptors | FreeBSD-specific extra facility | Not currently used by Guix, but relevant as a possible future supervision/containment primitive. | + +#### Assessment + +FreeBSD is not blocked at the basic process-creation layer. The earlier Guile subprocess crash was an ABI mismatch in Guile/gnulib usage, not a lack of kernel support for subprocess creation. + +### 2. File-descriptor cleanup and process hygiene + +| Guix/Linux expectation | FreeBSD status | Mapping / notes | +|---|---|---| +| `close_range` | Available and validated | Directly present in FreeBSD libc. | +| `closefrom` behavior | Available | Also native on FreeBSD and useful for daemon/build-helper hygiene. | +| `posix_spawn_file_actions_addclosefrom_np` | Available and validated | Strongly relevant because Guile and Guix subprocess helpers rely on this class of operation. | + +#### Assessment + +FreeBSD provides good native support for descriptor-sweeping operations. This is a positive compatibility point rather than a gap. + +### 3. Filesystem isolation and chroot-style build roots + +| Guix/Linux expectation | FreeBSD status | Mapping / notes | +|---|---|---| +| `chroot` build roots | Available and validated | Directly usable; root-only as expected. | +| bind-mount-style exposure of declared inputs | No Linux bind mounts, but equivalent behavior exists | Use `nullfs` mounts plus ordinary mount orchestration rather than Linux bind mounts. | +| `pivot_root` | Absent | Must not be relied on; jail/chroot/nullfs-based setup is the practical replacement direction. | + +#### Assessment + +The core “restricted filesystem root containing only declared paths” idea is achievable on FreeBSD, but the implementation must be rethought around `chroot`, `jail`, `mount`/`nmount`, and `nullfs`, rather than Linux mount namespaces plus `pivot_root`. + +### 4. Namespace-based isolation + +| Guix/Linux expectation | FreeBSD status | Mapping / notes | +|---|---|---| +| `clone(CLONE_NEW*)` | Absent | No direct equivalent. | +| `unshare` | Absent | No direct equivalent. | +| `setns` | Absent | No direct equivalent. | +| user namespaces | Absent in Linux sense | Must be replaced with jail design and traditional privilege separation. | +| mount namespaces | Absent in Linux sense | Must be replaced with jail/chroot + mount arrangement. | +| network namespaces | Absent in Linux sense | Use VNET jails when network isolation is required. | +| PID namespaces | Absent in Linux sense | Jail process isolation is the closest available model. | + +#### Assessment + +This is the single largest architectural gap between current Guix daemon code and FreeBSD. It confirms the Phase 2 design direction: Guix daemon isolation on FreeBSD cannot be a syscall-for-syscall translation; it must be a jail-oriented redesign. + +### 5. Mount and store exposure mechanics + +| Guix/Linux expectation | FreeBSD status | Mapping / notes | +|---|---|---| +| `mount` operations for build roots | Available | FreeBSD provides `mount(2)` and `nmount(2)`. | +| bind mounts | Different implementation model | `nullfs` is the practical analog for exposing existing paths elsewhere in the namespace. | +| recursive mount-namespace behavior | No direct equivalent | Must be handled explicitly in jail/chroot mount layout. | + +#### Assessment + +A FreeBSD Guix daemon will need explicit mount planning rather than namespace-based mount isolation. `nullfs` is the most natural replacement for the Linux bind-mount role in store/input exposure. + +### 6. Privilege dropping and capability models + +| Guix/Linux expectation | FreeBSD status | Mapping / notes | +|---|---|---| +| `setuid` / `setgid` build users | Available | Traditional Unix credential switching remains available. | +| Linux capabilities (`CAP_*`) | Absent | No direct equivalent. | +| `prctl`-style Linux process controls | Absent on this host | Must not be assumed. | +| seccomp filter model | Linux-specific | No direct equivalent. | +| Capsicum capability mode | Available on FreeBSD | Useful complementary mechanism, but not a 1:1 replacement for Linux capabilities or namespaces. | + +#### Assessment + +FreeBSD can still do classic build-user isolation, but the Linux capability/seccomp model must be replaced by a different combination of jails, traditional credentials, filesystem layout, and possibly Capsicum in carefully chosen places. + +### 7. Metadata, timestamps, and storage primitives + +| Guix/Linux expectation | FreeBSD status | Mapping / notes | +|---|---|---| +| `lchown` | Available and validated | Works in root test. | +| `lutimes` | Available and validated | Works. | +| `statvfs` | Available and validated | Works and is already referenced in current daemon code. | +| `statx` | Absent | Must use older `stat`/`lstat`/`fstatat` style interfaces instead. | +| `posix_fallocate` | Present but runtime-limited | Returned `EOPNOTSUPP` on the tested filesystems; presence does not imply useful semantics everywhere. | + +#### Assessment + +Most metadata operations map directly, but `statx` has no FreeBSD equivalent and `posix_fallocate` requires semantic caution rather than a simple availability check. + +## Porting implications for Phase 2 + +The system-call mapping work strongly supports the following Phase 2 design assumptions: + +1. **Use jails as the primary isolation model.** + Linux namespace code paths are not portable as-is. + +2. **Use `nullfs` + `chroot`/jail layout instead of Linux bind mounts + mount namespaces.** + +3. **Retain build users and classic UID/GID switching.** + These mechanisms remain directly usable on FreeBSD. + +4. **Do not depend on Linux seccomp/capability/prctl machinery.** + Any comparable restrictions must come from a different design. + +5. **Treat `posix_fallocate` conservatively.** + Configure-time presence is not enough; runtime filesystem behavior matters. + +## Conclusion + +Phase 1.3 is now satisfied by: + +- a concrete source-based mapping between Guix's Linux-oriented daemon assumptions and FreeBSD facilities +- a runnable C harness validating the most important FreeBSD-side primitives +- explicit identification of the irreducible architectural gap: Linux namespaces versus FreeBSD jails + +This provides enough detail for Phase 2 work to proceed with a jail-first design instead of attempting a misleading syscall-by-syscall translation. diff --git a/tests/system/freebsd-syscall-mapping.c b/tests/system/freebsd-syscall-mapping.c new file mode 100644 index 0000000..cf6dc08 --- /dev/null +++ b/tests/system/freebsd-syscall-mapping.c @@ -0,0 +1,565 @@ +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __FreeBSD__ +#include +#include +#endif + +extern char **environ; + +static int failures = 0; + +static void report_value(const char *key, const char *value) +{ + printf("%s=%s\n", key, value); +} + +static void report_ok(const char *key) +{ + printf("%s=ok\n", key); +} + +static void report_fail(const char *key, const char *detail) +{ + failures++; + printf("%s=fail:%s\n", key, detail); +} + +static void report_errno_detail(const char *key, const char *context) +{ + char buffer[256]; + snprintf(buffer, sizeof(buffer), "%s:%s", context, strerror(errno)); + report_fail(key, buffer); +} + +static void report_feature_macros(void) +{ +#ifdef SYS_clone + report_value("feature.SYS_clone", "yes"); +#else + report_value("feature.SYS_clone", "no"); +#endif + +#ifdef SYS_unshare + report_value("feature.SYS_unshare", "yes"); +#else + report_value("feature.SYS_unshare", "no"); +#endif + +#ifdef SYS_setns + report_value("feature.SYS_setns", "yes"); +#else + report_value("feature.SYS_setns", "no"); +#endif + +#ifdef SYS_pivot_root + report_value("feature.SYS_pivot_root", "yes"); +#else + report_value("feature.SYS_pivot_root", "no"); +#endif + +#ifdef __FreeBSD__ + report_value("feature.FreeBSD_jail", "yes"); +#else + report_value("feature.FreeBSD_jail", "no"); +#endif + +#if defined(__has_include) +# if __has_include() + report_value("feature.Capsicum_headers", "yes"); +# else + report_value("feature.Capsicum_headers", "no"); +# endif +# if __has_include() + report_value("feature.sys_prctl_h", "yes"); +# else + report_value("feature.sys_prctl_h", "no"); +# endif +# if __has_include() + report_value("feature.linux_close_range_h", "yes"); +# else + report_value("feature.linux_close_range_h", "no"); +# endif +#else + report_value("feature.Capsicum_headers", "unknown"); + report_value("feature.sys_prctl_h", "unknown"); + report_value("feature.linux_close_range_h", "unknown"); +#endif +} + +static void test_fork_waitpid(void) +{ + int fds[2]; + pid_t pid; + char buf[16] = {0}; + int status = 0; + + if (pipe(fds) != 0) { + report_errno_detail("runtime.fork_waitpid", "pipe"); + return; + } + + pid = fork(); + if (pid < 0) { + close(fds[0]); + close(fds[1]); + report_errno_detail("runtime.fork_waitpid", "fork"); + return; + } + + if (pid == 0) { + close(fds[0]); + (void)write(fds[1], "child-ok", 8); + close(fds[1]); + _exit(0); + } + + close(fds[1]); + if (read(fds[0], buf, sizeof(buf)) < 0) { + close(fds[0]); + report_errno_detail("runtime.fork_waitpid", "read"); + return; + } + close(fds[0]); + + if (waitpid(pid, &status, 0) < 0) { + report_errno_detail("runtime.fork_waitpid", "waitpid"); + return; + } + + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0 || strcmp(buf, "child-ok") != 0) { + report_fail("runtime.fork_waitpid", "unexpected-child-result"); + return; + } + + report_ok("runtime.fork_waitpid"); +} + +static void test_posix_spawn_closefrom(void) +{ + posix_spawn_file_actions_t actions; + pid_t pid = -1; + int status = 0; + + if (posix_spawn_file_actions_init(&actions) != 0) { + report_fail("runtime.posix_spawn_addclosefrom_np", "file-actions-init"); + return; + } + +#ifdef __FreeBSD__ + if (posix_spawn_file_actions_addclosefrom_np(&actions, 3) != 0) { + posix_spawn_file_actions_destroy(&actions); + report_fail("runtime.posix_spawn_addclosefrom_np", "addclosefrom_np"); + return; + } +#else + posix_spawn_file_actions_destroy(&actions); + report_value("runtime.posix_spawn_addclosefrom_np", "skipped"); + return; +#endif + + if (posix_spawn(&pid, "/usr/bin/true", &actions, NULL, + (char *const[]){(char *)"/usr/bin/true", NULL}, environ) != 0) { + posix_spawn_file_actions_destroy(&actions); + report_errno_detail("runtime.posix_spawn_addclosefrom_np", "posix_spawn"); + return; + } + + posix_spawn_file_actions_destroy(&actions); + + if (waitpid(pid, &status, 0) < 0) { + report_errno_detail("runtime.posix_spawn_addclosefrom_np", "waitpid"); + return; + } + + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + report_fail("runtime.posix_spawn_addclosefrom_np", "child-nonzero"); + return; + } + + report_ok("runtime.posix_spawn_addclosefrom_np"); +} + +static void test_close_range(void) +{ + int fds[2]; + int fd3; + + if (pipe(fds) != 0) { + report_errno_detail("runtime.close_range", "pipe"); + return; + } + + fd3 = dup(fds[0]); + if (fd3 < 0) { + close(fds[0]); + close(fds[1]); + report_errno_detail("runtime.close_range", "dup"); + return; + } + + if (close_range((unsigned int)fds[0], (unsigned int)fd3, 0) != 0) { + report_errno_detail("runtime.close_range", "close_range"); + return; + } + + if (fcntl(fds[0], F_GETFD) != -1 || errno != EBADF) { + report_fail("runtime.close_range", "fd-not-closed"); + return; + } + + report_ok("runtime.close_range"); +} + +static void test_posix_fallocate(void) +{ + char path[] = "/tmp/fruix-posix-fallocate.XXXXXX"; + int fd; + struct stat st; + + fd = mkstemp(path); + if (fd < 0) { + report_errno_detail("runtime.posix_fallocate", "mkstemp"); + return; + } + + { + int rc = posix_fallocate(fd, 0, 4096); + if (rc != 0) { + close(fd); + unlink(path); + if (rc == EOPNOTSUPP || rc == ENOTSUP) { + report_value("runtime.posix_fallocate", "unsupported-on-tested-filesystem"); + return; + } + report_fail("runtime.posix_fallocate", strerror(rc)); + return; + } + } + + if (fstat(fd, &st) != 0) { + close(fd); + unlink(path); + report_errno_detail("runtime.posix_fallocate", "fstat"); + return; + } + + close(fd); + unlink(path); + + if (st.st_size < 4096) { + report_fail("runtime.posix_fallocate", "short-size"); + return; + } + + report_ok("runtime.posix_fallocate"); +} + +static void test_lutimes(void) +{ + char dir[] = "/tmp/fruix-lutimes.XXXXXX"; + char target[PATH_MAX]; + char linkpath[PATH_MAX]; + struct timeval times[2]; + + if (mkdtemp(dir) == NULL) { + report_errno_detail("runtime.lutimes", "mkdtemp"); + return; + } + + snprintf(target, sizeof(target), "%s/target", dir); + snprintf(linkpath, sizeof(linkpath), "%s/link", dir); + + { + int fd = open(target, O_CREAT | O_WRONLY, 0600); + if (fd < 0) { + report_errno_detail("runtime.lutimes", "open-target"); + return; + } + close(fd); + } + + if (symlink(target, linkpath) != 0) { + report_errno_detail("runtime.lutimes", "symlink"); + unlink(target); + rmdir(dir); + return; + } + + times[0].tv_sec = 1; + times[0].tv_usec = 0; + times[1].tv_sec = 2; + times[1].tv_usec = 0; + + if (lutimes(linkpath, times) != 0) { + report_errno_detail("runtime.lutimes", "lutimes"); + unlink(linkpath); + unlink(target); + rmdir(dir); + return; + } + + unlink(linkpath); + unlink(target); + rmdir(dir); + report_ok("runtime.lutimes"); +} + +static void test_statvfs(void) +{ + struct statvfs st; + + if (statvfs("/tmp", &st) != 0) { + report_errno_detail("runtime.statvfs", "statvfs"); + return; + } + + if (st.f_bsize == 0) { + report_fail("runtime.statvfs", "zero-block-size"); + return; + } + + report_ok("runtime.statvfs"); +} + +static void test_chroot_smoke(void) +{ + char top[] = "/tmp/fruix-chroot.XXXXXX"; + char rootdir[PATH_MAX]; + char etcdir[PATH_MAX]; + char passwdpath[PATH_MAX]; + char outside[PATH_MAX]; + pid_t pid; + int status = 0; + + if (mkdtemp(top) == NULL) { + report_errno_detail("root.chroot", "mkdtemp"); + return; + } + + snprintf(rootdir, sizeof(rootdir), "%s/root", top); + snprintf(etcdir, sizeof(etcdir), "%s/etc", rootdir); + snprintf(passwdpath, sizeof(passwdpath), "%s/passwd", etcdir); + snprintf(outside, sizeof(outside), "%s/outside-sentinel", top); + + if (mkdir(rootdir, 0755) != 0 || mkdir(etcdir, 0755) != 0) { + report_errno_detail("root.chroot", "mkdir"); + return; + } + + { + int fd = open(passwdpath, O_CREAT | O_WRONLY, 0644); + if (fd < 0) { + report_errno_detail("root.chroot", "open-passwd"); + return; + } + (void)write(fd, "root:x:0:0::/:/bin/sh\n", 22); + close(fd); + } + + { + int fd = open(outside, O_CREAT | O_WRONLY, 0644); + if (fd < 0) { + report_errno_detail("root.chroot", "open-outside"); + return; + } + close(fd); + } + + pid = fork(); + if (pid < 0) { + report_errno_detail("root.chroot", "fork"); + return; + } + + if (pid == 0) { + if (chdir(rootdir) != 0) + _exit(10); + if (chroot(".") != 0) + _exit(11); + if (access("/etc/passwd", F_OK) != 0) + _exit(12); + if (access("/outside-sentinel", F_OK) == 0) + _exit(13); + _exit(0); + } + + if (waitpid(pid, &status, 0) < 0) { + report_errno_detail("root.chroot", "waitpid"); + return; + } + + unlink(outside); + unlink(passwdpath); + rmdir(etcdir); + rmdir(rootdir); + rmdir(top); + + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + report_fail("root.chroot", "child-failed"); + return; + } + + report_ok("root.chroot"); +} + +#ifdef __FreeBSD__ +static void test_jail_smoke(void) +{ + char top[] = "/tmp/fruix-jail.XXXXXX"; + struct jail j; + pid_t pid; + int status = 0; + + if (mkdtemp(top) == NULL) { + report_errno_detail("root.jail", "mkdtemp"); + return; + } + + pid = fork(); + if (pid < 0) { + report_errno_detail("root.jail", "fork"); + rmdir(top); + return; + } + + if (pid == 0) { + char hostname[] = "fruix-jail"; + char jailname[] = "fruix-jail"; + char observed[256] = {0}; + + memset(&j, 0, sizeof(j)); + j.version = JAIL_API_VERSION; + j.path = top; + j.hostname = hostname; + j.jailname = jailname; + j.ip4s = 0; + j.ip6s = 0; + j.ip4 = NULL; + j.ip6 = NULL; + + if (jail(&j) < 0) + _exit(20); + if (gethostname(observed, sizeof(observed)) != 0) + _exit(21); + if (strcmp(observed, hostname) != 0) + _exit(22); + _exit(0); + } + + if (waitpid(pid, &status, 0) < 0) { + report_errno_detail("root.jail", "waitpid"); + rmdir(top); + return; + } + + rmdir(top); + + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { + report_fail("root.jail", "child-failed"); + return; + } + + report_ok("root.jail"); +} +#else +static void test_jail_smoke(void) +{ + report_value("root.jail", "skipped"); +} +#endif + +static void test_lchown_root(void) +{ + char dir[] = "/tmp/fruix-lchown.XXXXXX"; + char target[PATH_MAX]; + char linkpath[PATH_MAX]; + + if (mkdtemp(dir) == NULL) { + report_errno_detail("root.lchown", "mkdtemp"); + return; + } + + snprintf(target, sizeof(target), "%s/target", dir); + snprintf(linkpath, sizeof(linkpath), "%s/link", dir); + + { + int fd = open(target, O_CREAT | O_WRONLY, 0600); + if (fd < 0) { + report_errno_detail("root.lchown", "open-target"); + return; + } + close(fd); + } + + if (symlink(target, linkpath) != 0) { + report_errno_detail("root.lchown", "symlink"); + unlink(target); + rmdir(dir); + return; + } + + if (lchown(linkpath, getuid(), getgid()) != 0) { + report_errno_detail("root.lchown", "lchown"); + unlink(linkpath); + unlink(target); + rmdir(dir); + return; + } + + unlink(linkpath); + unlink(target); + rmdir(dir); + report_ok("root.lchown"); +} + +static int run_regular_tests(void) +{ + report_feature_macros(); + test_fork_waitpid(); + test_posix_spawn_closefrom(); + test_close_range(); + test_posix_fallocate(); + test_lutimes(); + test_statvfs(); + return failures == 0 ? 0 : 1; +} + +static int run_root_tests(void) +{ + if (geteuid() != 0) { + fprintf(stderr, "root tests require euid 0\n"); + return 2; + } + + test_chroot_smoke(); + test_jail_smoke(); + test_lchown_root(); + return failures == 0 ? 0 : 1; +} + +int main(int argc, char **argv) +{ + if (argc > 1 && strcmp(argv[1], "--root-tests") == 0) + return run_root_tests(); + + return run_regular_tests(); +} diff --git a/tests/system/run-freebsd-syscall-mapping.sh b/tests/system/run-freebsd-syscall-mapping.sh new file mode 100755 index 0000000..c87f2da --- /dev/null +++ b/tests/system/run-freebsd-syscall-mapping.sh @@ -0,0 +1,104 @@ +#!/bin/sh +set -eu + +cc_bin=${CC_BIN:-cc} +source_file=${SOURCE_FILE:-tests/system/freebsd-syscall-mapping.c} + +if ! command -v "$cc_bin" >/dev/null 2>&1; then + echo "C compiler not found: $cc_bin" >&2 + exit 1 +fi +if [ ! -f "$source_file" ]; then + echo "Source file not found: $source_file" >&2 + exit 1 +fi + +cleanup=0 +if [ -n "${WORKDIR:-}" ]; then + workdir=$WORKDIR + mkdir -p "$workdir" +else + workdir=$(mktemp -d /tmp/fruix-freebsd-syscall-mapping.XXXXXX) + cleanup=1 +fi + +if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then + cleanup=0 +fi + +cleanup_workdir() { + if [ "$cleanup" -eq 1 ]; then + rm -rf "$workdir" + fi +} +trap cleanup_workdir EXIT INT TERM + +binary=$workdir/freebsd-syscall-mapping +compile_log=$workdir/compile.log +regular_out=$workdir/regular.out +regular_err=$workdir/regular.err +root_out=$workdir/root.out +root_err=$workdir/root.err +metadata_file=$workdir/freebsd-syscall-mapping-metadata.txt + +printf 'Working directory: %s\n' "$workdir" +printf 'Compiler: %s\n' "$cc_bin" + +"$cc_bin" -Wall -Wextra -std=c11 -I/usr/local/include "$source_file" -o "$binary" >"$compile_log" 2>&1 + +set +e +"$binary" >"$regular_out" 2>"$regular_err" +regular_rc=$? +set -e +if [ "$regular_rc" -ne 0 ]; then + echo "regular syscall mapping checks failed" >&2 + cat "$regular_out" >&2 || true + cat "$regular_err" >&2 || true + exit 1 +fi + +set +e +sudo "$binary" --root-tests >"$root_out" 2>"$root_err" +root_rc=$? +set -e +if [ "$root_rc" -ne 0 ]; then + echo "root syscall mapping checks failed" >&2 + cat "$root_out" >&2 || true + cat "$root_err" >&2 || true + exit 1 +fi + +cat >"$metadata_file" <