ci: let the runner execute what it builds

The upstream gitea-actions-runner unit uses DynamicUser, and systemd
mounts a dynamic unit's state directory noexec. Measured from a job's
own /proc/self/mountinfo on the live runner, with /tmp as the control:
the state dir carries noexec, /tmp does not, and a script written and
chmod +x'd in the workspace fails execve with EACCES while the same
script in /tmp runs.

This was invisible for as long as every workflow compiled inside the
nix sandbox and executed nothing from the workspace. The first job that
built a binary into the runner's own target dir -- an instrumented
coverage run -- died on its first build script.

ExecPaths= re-mounts the state dir executable. Both spellings are
listed with the ignore-if-absent prefix because ExecPaths resolves
against the host root, where the dynamic-user layout makes
/var/lib/gitea-runner a symlink into private/.
This commit is contained in:
atlas 2026-08-27 14:48:47 +02:00
commit 5a5a4ddd15

View file

@ -355,6 +355,31 @@ in
systemd.services."gitea-runner-hive" = {
path = [ pkgs.nix ];
# A CI runner must be able to EXECUTE what it builds, and by
# default it cannot: the upstream unit uses `DynamicUser`, and
# systemd mounts a dynamic unit's state directory `noexec`.
# Measured on the live runner rather than inferred — from a job's
# own /proc/self/mountinfo:
#
# /var/lib/private ro,nosuid,nodev,noexec (tmpfs)
# /var/lib/private/gitea-runner rw,nosuid,nodev,noexec,idmapped
#
# with /tmp as the control at rw,nosuid,nodev — no noexec, and a
# script there executes fine. Nothing noticed for as long as every
# workflow built inside the nix sandbox and executed nothing in the
# workspace; the first job that compiled a build script into the
# runner's own target dir died with EACCES on execve, reported as
# "could not execute process ... (never executed)".
#
# Both spellings are listed because `ExecPaths=` resolves against
# the HOST root, where the dynamic-user layout makes
# /var/lib/gitea-runner a symlink into private/. The `-` prefix
# means "ignore if absent", so whichever layout the unit ends up
# with, the other is a no-op rather than a startup failure.
serviceConfig.ExecPaths = [
"-/var/lib/gitea-runner"
"-/var/lib/private/gitea-runner"
];
# Trust the hive CA in Node-based actions. With self-signed TLS,
# forgejo's ROOT_URL is `https://forge.<domain>` (CA-signed leaf),
# so actions like `upload-artifact` — whose Node HTTP client uses