Skip to content

Runtime Executable Deny Cannot Be Securely Implemented on Multi-Call Binary Systemen #67

@dwt

Description

@dwt

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions