Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
22 changes: 17 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions include/apg/scripts.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
19 changes: 16 additions & 3 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions meson_options.txt
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
61 changes: 59 additions & 2 deletions src/install/scripts.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

#ifdef __linux__
#include <sched.h>
#elif defined(__FreeBSD__)
#include <fcntl.h>
#include <sys/capsicum.h>
#endif

#include "../../include/apg/scripts.h"
Expand Down Expand Up @@ -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);
Expand Down
Loading