Summary
The Runtime Executable Deny functionality in Fence has a fundamental design limitation on systems with multi-call
binaries (Nix, BusyBox, Alpine, etc.). The current implementation either:
• Blocks too much (entire bundle like coreutils → ls, cat, dd all blocked)
• Or can be bypassed (via --coreutils-prog= or environment variables)
Technical Background
Issue 1: Kernel Resolves Symlinks
# On Nix-Darwin:
/run/current-system/sw/bin/dd → /nix/store/...-coreutils/bin/coreutils
# Kernel execve() behavior:
# 1. Symlink is resolved
# 2. /proc/PID/exe points to coreutils (not dd!)
# 3. sandbox-exec / Landlock only see "coreutils"
Consequence: Blocking coreutils blocks all commands in the bundle (ls, cat, cp, mv, rm, ...).
Issue 2: $0 Is Not Trustworthy
# Theoretically one could check argv[0]:
argv[0] = "/run/current-system/sw/bin/dd"
# BUT: $0 is easily spoofed!
bash -c "exec -a /usr/bin/python3 /nix/store/...-coreutils/bin/coreutils"
# → Process appears as "python3" even though it's coreutils
Issue 3: coreutils --coreutils-prog=dd Bypass
# GNU coreutils supports explicit program selection:
coreutils --coreutils-prog=dd if=/dev/zero of=/tmp/test
# Also:
coreutils --coreutils-prog=ls -la
coreutils --coreutils-prog=cat /etc/passwd
Consequence: Even if dd is blocked, coreutils can be executed and act as dd.
Issue 4: Additional Bypasses
# BusyBox / ToyBox similarly:
busybox dd if=/dev/zero
toybox ls -la
# Shell functions / aliases:
function dd() { coreutils --coreutils-prog=dd "$@"; }
# Symlinks with different names:
ln -s /nix/store/...-coreutils/bin/coreutils /tmp/my-dd
/tmp/my-dd if=/dev/zero
Current Implementation (Fence)
Preflight Command Parsing ( command.go )
• ✅ Works on all systems
• ✅ Text-based, no paths involved
• ✅ Blocks "dd if=/dev/zero" against "dd if=" rule
• ❌ Can be bypassed with complex shell constructs
Runtime Executable Deny ( runtime_exec_deny.go )
• ✅ Blocks child processes (e.g., env python3 )
• ✅ Works well on systems with single-purpose binaries
• ❌ Multi-call systems: Either blocks everything or nothing
• ❌ Cannot distinguish dd from ls when both → coreutils
What to do?
First question is probably... how is this supposed to work? Are the two blocking layers (command parsing based and execution based) only meant as defense in depth and we only want them to be good enough to block some things?
In that case we could try to have a kind of whitelist that excludes multi call binaries like busybox, coreutils, python and a few others?
If these layers are meant to be as good as possible.... I am not sure what could be done in that case actually.
In any case, this was all triggered because /nix/store triggers an additional complexity with sandboxing that I hadn't thought about before.
I would appreciate your Input @jy-tan to understand how this is meant to work so I / we can prepare a solution.
Summary
The Runtime Executable Deny functionality in Fence has a fundamental design limitation on systems with multi-call
binaries (Nix, BusyBox, Alpine, etc.). The current implementation either:
• Blocks too much (entire bundle like coreutils → ls, cat, dd all blocked)
• Or can be bypassed (via --coreutils-prog= or environment variables)
Technical Background
Issue 1: Kernel Resolves Symlinks
Consequence: Blocking coreutils blocks all commands in the bundle (ls, cat, cp, mv, rm, ...).
Issue 2: $0 Is Not Trustworthy
Issue 3: coreutils --coreutils-prog=dd Bypass
Consequence: Even if dd is blocked, coreutils can be executed and act as dd.
Issue 4: Additional Bypasses
Current Implementation (Fence)
Preflight Command Parsing ( command.go )
• ✅ Works on all systems
• ✅ Text-based, no paths involved
• ✅ Blocks "dd if=/dev/zero" against "dd if=" rule
• ❌ Can be bypassed with complex shell constructs
Runtime Executable Deny ( runtime_exec_deny.go )
• ✅ Blocks child processes (e.g., env python3 )
• ✅ Works well on systems with single-purpose binaries
• ❌ Multi-call systems: Either blocks everything or nothing
• ❌ Cannot distinguish dd from ls when both → coreutils
What to do?
First question is probably... how is this supposed to work? Are the two blocking layers (command parsing based and execution based) only meant as defense in depth and we only want them to be good enough to block some things?
In that case we could try to have a kind of whitelist that excludes multi call binaries like
busybox,coreutils,pythonand a few others?If these layers are meant to be as good as possible.... I am not sure what could be done in that case actually.
In any case, this was all triggered because /nix/store triggers an additional complexity with sandboxing that I hadn't thought about before.
I would appreciate your Input @jy-tan to understand how this is meant to work so I / we can prepare a solution.