diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee55750..2e48fc0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -379,7 +379,7 @@ jobs: architecture: x86-64 version: '14.3' run: | - sudo pkg install -y meson pkgconf ninja lmdb libarchive yyjson libsodium + sudo pkg install -y meson pkgconf ninja lmdb libarchive yyjson libsodium gpgme meson setup build --buildtype=release meson compile -C build meson test -C build diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ab8928..6eb0380 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,12 +19,24 @@ All notable changes to this project will be documented in this file. `cross-riscv32.txt`, `cross-mips.txt`, `cross-powerpc.txt`) and CI coverage: an `arch-asm` job that assembles and PIC-links every 32-bit backend, plus a full `armhf` cross-compile job -- FreeBSD support: the library builds, links, and passes its test suite on - FreeBSD with the libsodium signing backend; no source changes are required - because the seccomp dependency is optional and the install-script runner - already has a non-Linux path +- FreeBSD install-script sandbox: `run_script()` now isolates scripts on + FreeBSD using Capsicum capability mode (`cap_enter()`); the script binary is + opened before entering capability mode and executed with `fexecve()`, and + sandbox failure is fail-closed, matching the Linux `unshare()` guarantee - CI job that builds and tests the library in a native FreeBSD x86_64 virtual - machine + machine, now with the `gpgme` signing backend installed so the preferred + backend gets the same test coverage it has on Linux + +### Fixed + +- `-D_GNU_SOURCE` is now only added on Linux; it was previously applied + unconditionally, which is a glibc-specific feature macro with no meaning on + BSD libc +- Default `keyring_dir` and `keys_dir` now resolve under `/usr/local/etc/apg/` + on FreeBSD instead of `/etc/apg/`, following FreeBSD's `hier(7)` convention + for locally-installed configuration +- README documents FreeBSD `pkg install` dependencies and the platform's + sandbox and configuration-path differences ## [1.9.0] - 2026-06-22 diff --git a/README.md b/README.md index 996bb5e..7960d44 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Package management library for NurOS. | [lmdb](https://www.symas.com/lmdb) | Embedded key-value database | | [yyjson](https://github.com/ibireme/yyjson) | JSON library | | [gpgme](https://www.gnupg.org/related_software/gpgme/) **or** [libsodium](https://libsodium.org/) | Package signing | -| [libseccomp](https://github.com/seccomp/libseccomp) *(optional)* | Syscall filtering for install script sandbox | +| [libseccomp](https://github.com/seccomp/libseccomp) *(optional, Linux only)* | Syscall filtering for install script sandbox | ## Signing backends @@ -22,6 +22,16 @@ libapg supports two signing backends. The build system picks the first one avail - **gpgme** (preferred) — PGP signing via GnuPG. Only ECC keys are accepted by default (Ed25519, ECDSA). RSA can be enabled explicitly by passing `allow_rsa = true` to `sign_verify`. - **libsodium** (fallback) — Ed25519 signing. Always ECC, keys are read from `/etc/apg/keys/`. +## Install-script sandbox + +`run_script()` isolates pre/post-install scripts using the strongest primitive available on the host: + +- **Linux** — `unshare()` with isolated network, mount, UTS, and IPC namespaces, plus optional `libseccomp` syscall filtering. +- **FreeBSD** — [Capsicum](https://man.freebsd.org/cgi/man.cgi?query=capsicum) capability mode (`cap_enter()`). The script binary is opened before entering capability mode and executed with `fexecve()`; once sandboxed, the process loses access to global namespaces (path-based lookups, most syscalls). +- **Other POSIX platforms** — no sandbox primitive is available; scripts run without isolation. + +On Linux and FreeBSD, if the sandbox cannot be established the script is not executed (fail closed). + ## Building ### With Nix @@ -74,6 +84,14 @@ sudo emerge dev-build/meson dev-build/ninja dev-util/pkgconf app-arch/libarchive sudo xbps-install meson ninja pkgconf libarchive-devel lmdb-devel gpgme-devel ``` +#### FreeBSD + +```bash +sudo pkg install meson pkgconf ninja lmdb libarchive yyjson gpgme +``` + +`libseccomp` is not available on FreeBSD; the install-script sandbox uses Capsicum instead (see [Install-script sandbox](#install-script-sandbox)). Default configuration paths follow FreeBSD's `hier(7)` and resolve under `/usr/local/etc/apg/` instead of `/etc/apg/`. + #### Build ```bash diff --git a/include/apg/scripts.h b/include/apg/scripts.h index e5b5535..04ad94a 100644 --- a/include/apg/scripts.h +++ b/include/apg/scripts.h @@ -14,9 +14,14 @@ * @brief Run a lifecycle script from a package's @c scripts/ directory. * * Executes @c pkg_dir/scripts/name if the file exists and is executable. - * The script runs in a sandboxed child process with isolated network, mount, - * UTS, and IPC namespaces. If the sandbox cannot be established the script - * is not executed and the call returns false (fail closed). + * On Linux, the script runs in a child process with isolated network, mount, + * UTS, and IPC namespaces (@c unshare()). On FreeBSD, the script runs under + * Capsicum capability mode (@c cap_enter()), which drops access to global + * namespaces (path-based lookups, most syscalls) after the executable is + * opened. On both platforms, if the sandbox cannot be established the script + * is not executed and the call returns false (fail closed). On other POSIX + * platforms, no sandbox primitive is available and the script runs without + * isolation. * Common script names are @c pre-install, @c post-install, @c pre-remove, * and @c post-remove. * diff --git a/meson.build b/meson.build index 9234eda..2976888 100644 --- a/meson.build +++ b/meson.build @@ -35,11 +35,24 @@ libapg_sources = files( 'src/transaction/commit.c', ) -add_project_arguments('-D_GNU_SOURCE', language: 'c') +if host_machine.system() == 'linux' + add_project_arguments('-D_GNU_SOURCE', language: 'c') +endif + +keyring_dir = get_option('keyring_dir') +keys_dir = get_option('keys_dir') +if host_machine.system() == 'freebsd' + if keyring_dir == '/etc/apg/trusted.d' + keyring_dir = '/usr/local/etc/apg/trusted.d' + endif + if keys_dir == '/etc/apg/keys' + keys_dir = '/usr/local/etc/apg/keys' + endif +endif add_project_arguments( - '-DAPG_KEYRING_DIR="' + get_option('keyring_dir') + '"', - '-DAPG_KEYS_DIR="' + get_option('keys_dir') + '"', + '-DAPG_KEYRING_DIR="' + keyring_dir + '"', + '-DAPG_KEYS_DIR="' + keys_dir + '"', '-DAPG_TMP_DIR="' + get_option('tmp_dir') + '"', '-DAPG_DB_MAPSIZE=' + get_option('db_mapsize').to_string(), language: 'c', diff --git a/meson_options.txt b/meson_options.txt index aab2db9..89daf23 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,13 +1,13 @@ option('keyring_dir', type: 'string', value: '/etc/apg/trusted.d', - description: 'Default directory for trusted signing keys', + description: 'Default directory for trusted signing keys (auto-adjusted to /usr/local/etc/apg/trusted.d on FreeBSD unless overridden)', ) option('keys_dir', type: 'string', value: '/etc/apg/keys', - description: 'Default directory for libsodium key files', + description: 'Default directory for libsodium key files (auto-adjusted to /usr/local/etc/apg/keys on FreeBSD unless overridden)', ) option('tmp_dir', diff --git a/src/install/scripts.c b/src/install/scripts.c index f2c8753..26fcdda 100644 --- a/src/install/scripts.c +++ b/src/install/scripts.c @@ -11,6 +11,9 @@ #ifdef __linux__ #include +#elif defined(__FreeBSD__) +#include +#include #endif #include "../../include/apg/scripts.h" @@ -64,8 +67,62 @@ exec_script(const char *path) _exit(1); } - // Parent: close write end and check whether child signalled sandbox - // failure. + close(pipefd[1]); + uint8_t err = 0; + ssize_t n = read(pipefd[0], &err, 1); + close(pipefd[0]); + + if (n > 0) + { + waitpid(pid, NULL, 0); + return false; + } + + int status; + if (waitpid(pid, &status, 0) < 0) + return false; + return WIFEXITED(status) && WEXITSTATUS(status) == 0; +#elif defined(__FreeBSD__) + int fd = open(path, O_EXEC); + if (fd < 0) + return false; + + int pipefd[2]; + if (pipe(pipefd) < 0) + { + close(fd); + return false; + } + + pid_t pid = fork(); + if (pid < 0) + { + close(fd); + close(pipefd[0]); + close(pipefd[1]); + return false; + } + + if (pid == 0) + { + close(pipefd[0]); + + if (cap_enter() < 0) + { + uint8_t err = 1; + (void)write(pipefd[1], &err, 1); + close(pipefd[1]); + _exit(1); + } + + close(pipefd[1]); + char *const argv[] = {(char *)path, NULL}; + char *const envp[] = {NULL}; + fexecve(fd, argv, envp); + _exit(1); + } + + close(fd); close(pipefd[1]); uint8_t err = 0; ssize_t n = read(pipefd[0], &err, 1);