From 5a5a4ddd1513306d02f6439401235d99f657a149 Mon Sep 17 00:00:00 2001 From: atlas Date: Thu, 27 Aug 2026 14:48:47 +0200 Subject: [PATCH] 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/. --- nix/host-modules/hive-ci.nix | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/nix/host-modules/hive-ci.nix b/nix/host-modules/hive-ci.nix index 7af88be9..b33fb22d 100644 --- a/nix/host-modules/hive-ci.nix +++ b/nix/host-modules/hive-ci.nix @@ -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.` (CA-signed leaf), # so actions like `upload-artifact` — whose Node HTTP client uses