Skip to content

shared: Run scripts from a file when no executable memfd is available - #1014

Merged
stgraber merged 1 commit into
lxc:mainfrom
initd-it:shared-runscript-noexec-memfd
Oct 4, 2026
Merged

stgraber merged 1 commit into
lxc:mainfrom
initd-it:shared-runscript-noexec-memfd

Conversation

@initd-it

@initd-it initd-it commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

RunScript runs every action script (and a few helper scripts of the sources) by writing it to a memfd and executing /proc/self/fd/N. That can't work on hosts hardened with vm.memfd_noexec=2 (or =1): a memfd created with flags 0 isn't executable, and at level 2 memfd_create() refuses MFD_EXEC too. Any build with an action script fails right after the base system is unpacked:

Error: Failed to run post-unpack: fork/exec /proc/self/fd/6: permission denied

Changes:

  • the memfd is requested with MFD_EXEC (so vm.memfd_noexec=1 keeps working without touching the disk), retrying with flags 0 on EINVAL (kernels older than 6.3, which don't have the sysctl either);
  • when no executable memfd can be created, the script is written to a temporary file (0700, created with O_EXCL), closed, executed and removed. This is what @stgraber suggested in Added support for changing interpreter with shebang in RunScript #395 ("create a temporary file (possibly memfd or something like that) and then execute it the normal way. This will then let the kernel sort out the shebang") and what LXC's exec_as_memfd does.

Nothing changes where an executable memfd is available.

Tested:

  • new TestRunScript (memfd, memfd without MFD_EXEC support, memfd denied, exit codes, shebang options, no leftover file). It runs unprivileged and passes with vm.memfd_noexec=0 (kernel 7.0) and vm.memfd_noexec=2 (kernel 6.12);
  • Debian 13 host (kernel 6.12) with vm.memfd_noexec=2: distrobuilder 3.2 from Debian fails as above; this branch, on top of main, builds a debootstrap trixie (minbase) image with post-unpack, post-packages and post-files actions. The three scripts run from /tmp/.distrobuilder-script-* inside the chroot, nothing is left in the image, and dmesg shows the expected refusal for each script;
  • not tested with vm.memfd_noexec=1 (covered by the kernel semantics only).

Notes:

  • with vm.memfd_noexec=2 on kernels >= 6.6 every script logs one kernel error line (the refused MFD_EXEC attempt). Remembering the failure for the process lifetime would avoid it; I left it out to avoid global state, happy to add it;
  • the fallback only covers memfd creation failures, not a memfd that is created but denied at exec time by an LSM;
  • the temporary file goes in os.TempDir(): inside the chroot that's the tmpfs distrobuilder mounts on /tmp, for the four sources that run on the host (busybox, openwrt, slackware, plamolinux) it's the host's /tmp, which must allow exec. TMPDIR is honoured, so it should name a path that exists in the chroot too.

RunScript executes scripts through a memfd. With vm.memfd_noexec=2, memfd_create() refuses to create executable memfds, and with vm.memfd_noexec=1 a memfd created without MFD_EXEC can't be executed. Building an image with action scripts then fails, for example with "fork/exec /proc/self/fd/N: permission denied".

Request an executable memfd with MFD_EXEC, retrying without it on kernels older than 6.3. When no executable memfd can be created, write the script to a temporary file, execute it and remove it afterwards. The kernel still takes care of the shebang.

Signed-off-by: Alessandro Corbelli <git@initd.it>
@stgraber
stgraber merged commit 20b6d66 into lxc:main Oct 4, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants