Skip to content

fix: skip runtime exec deny for shared executable targets#70

Open
dwt wants to merge 2 commits intoUse-Tusk:mainfrom
dwt:fix-bundled-binaries-overblock
Open

fix: skip runtime exec deny for shared executable targets#70
dwt wants to merge 2 commits intoUse-Tusk:mainfrom
dwt:fix-bundled-binaries-overblock

Conversation

@dwt
Copy link
Collaborator

@dwt dwt commented Mar 11, 2026

Detect shared executable targets by file identity and skip runtime path masking when a deny would block multiple command names, with debug diagnostics on Linux and macOS.

Not sure this is the right way to go about this, but at least that does fix my problem.

To describe again:

Multi call binaries (like busybox, modern coreutils, python, …) have many symlinks that point to the same binary and perhaps work differently depending on how they are called.

So for coreutils, dd is a symlink to it, but so does ls, cat, … So blocking coreutils because dd resolves to it is not a good idea.

Closes: #67

@dwt dwt requested a review from jy-tan as a code owner March 11, 2026 18:40
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 5 files

@dwt dwt force-pushed the fix-bundled-binaries-overblock branch from e8c5d3e to 136d530 Compare March 14, 2026 12:02
@dwt
Copy link
Collaborator Author

dwt commented Mar 24, 2026

Fence: Smart Shared-Binary Exec-Deny

2026-03-24T18:43:40Z by Showboat 0.6.1

Background

On Nix and nix-darwin every coreutils command (ls, cat, dd, head, tail, ...) is a symlink
pointing at a single multicall binary. The same pattern appears with uutils-coreutils and busybox.

When fence resolves symlinks before building its runtime exec-deny list, blocking dd resolves to
that shared binary — which also implements ls, cat, and ~100 other commands. Without protection,
the sandbox environment breaks entirely.

This document verifies the fix: the new logic detects critical-command collisions and handles them
with three distinct behaviours, while still blocking non-critical shared binaries (e.g. python3) normally.

Versions under test

echo "system fence:" && fence --version 2>&1 | grep "Version:" && echo "local fence (this branch):" && ./fence --version 2>&1 | grep "Version:"
system fence:
  Version: 0.1.32
local fence (this branch):
  Version: dev

The shared-binary situation on this machine

dd and ls are both Nix coreutils symlinks. Verify they resolve to the same canonical binary:

echo "dd  -> $(which dd) -> $(readlink $(which dd))" && echo "ls  -> $(which ls) -> $(readlink $(which ls))" && echo && echo "canonical dd: $(realpath $(which dd))" && echo "canonical ls: $(realpath $(which ls))" && echo && echo "commands sharing that binary: $(ls $(dirname $(realpath $(which dd))) | wc -l | tr -d " ") (including the coreutils binary itself)"
dd  -> /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/dd -> coreutils
ls  -> /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/ls -> coreutils

canonical dd: /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/coreutils
canonical ls: /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/coreutils

commands sharing that binary: 107 (including the coreutils binary itself)

106 symlinks + 1 real binary, all sharing one inode. Any path fence adds to its exec-deny list that resolves to this binary will block all 106 commands at once.

The bug: system fence (v0.1.32) breaks ls when dd is denied

Config: {"command":{"deny":["dd"],"useDefaults":false}}

Without the fix, resolving dd → coreutils canonical path → that path is added to the OS exec-deny list → every one of the 106 other coreutils symlinks (ls, cat, head, tail, ...) is collaterally blocked.

fence --settings bundled-binaries/config-deny-dd.json -c "ls /tmp" 2>&1; echo "exit: $?"
/nix/store/s0psayl7zvkvwdcqc8fy1sbv8rlf1yq8-bash-5.3p9/bin/bash: line 1: /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/ls: Operation not permitted
exit: 126

The sandbox blocks ls (exit 126) because it resolves to the same coreutils binary that dd was asked to deny. The user never asked to block ls.

Fix: local fence (dev) detects the critical collision and skips the coreutils path

Same config. The new logic detects that the coreutils canonical path also implements ls, cat, head, tail, and ~100 other critical commands — and skips adding it to the exec-deny list. The standalone macOS /bin/dd (a separate binary) is still blocked.

./fence --settings bundled-binaries/config-deny-dd.json -c "ls /tmp" 2>&1; echo "exit: $?"
MozillaUpdateLock-83AB270330835640
com.apple.launchd.GhgLYCyvQZ
com.apple.launchd.s8WZbe4DCH
fence
powerlog
textmate-501.sock
zeb_def_ipc_3503
zeb_def_ipc_7450
exit: 0

ls runs cleanly. The --debug flag logs an actionable diagnostic explaining exactly what happened and how to resolve it:

./fence --debug --settings bundled-binaries/config-deny-dd.json -c "ls /tmp" 2>&1 | grep "fence:macos"
[fence:macos] runtime exec deny skipped for /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/coreutils (requested: dd): shared binary also implements critical commands [[ basename cat cp cut date dirname echo env false head id ls mkdir mktemp mv printf pwd readlink realpath rmdir sort tail tee test touch tr true uname uniq wc whoami]. To force blocking add "allowBlockingCritical": true to your command config. To silence this warning add "dd" to "acknowledgeSharedBinary".

Opt-in: allowBlockingCritical: true forces the block anyway

Config: {"command":{"deny":["dd"],"useDefaults":false,"allowBlockingCritical":true}}

The user has explicitly accepted the risk. The coreutils path is added to the deny list — ls is blocked, no diagnostic is emitted.

./fence --settings bundled-binaries/config-deny-dd-force.json -c "ls /tmp" 2>&1; echo "exit: $?"
/nix/store/s0psayl7zvkvwdcqc8fy1sbv8rlf1yq8-bash-5.3p9/bin/bash: line 1: /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/ls: Operation not permitted
exit: 126

Opt-out: acknowledgeSharedBinary: ["dd"] silences the warning

Config: {"command":{"deny":["dd"],"useDefaults":false,"acknowledgeSharedBinary":["dd"]}}

The user has investigated, knows dd cannot be isolated on this system, and wants no noise about it. The coreutils path is silently skipped — ls works, and --debug logs nothing for this path.

./fence --settings bundled-binaries/config-deny-dd-ack.json -c "ls /tmp" 2>&1; echo "exit: $?"
MozillaUpdateLock-83AB270330835640
com.apple.launchd.GhgLYCyvQZ
com.apple.launchd.s8WZbe4DCH
fence
powerlog
textmate-501.sock
zeb_def_ipc_3503
zeb_def_ipc_7450
exit: 0
./fence --debug --settings bundled-binaries/config-deny-dd-ack.json -c "ls /tmp" 2>&1 | grep "fence:macos" || echo "(no diagnostic logged)"
(no diagnostic logged)

Non-critical shared binary: python3 is still blocked

Config: {"command":{"deny":["python3"],"useDefaults":false}}

python3, python3.13, and python3-config share a single binary on this machine — but none of those names are on the critical-commands list. There is no collateral damage to worry about, so the runtime exec-deny proceeds normally. ls is completely unaffected.

./fence --settings bundled-binaries/config-deny-python3.json -c "ls /tmp" 2>&1; echo "exit: $?"
MozillaUpdateLock-83AB270330835640
com.apple.launchd.GhgLYCyvQZ
com.apple.launchd.s8WZbe4DCH
fence
powerlog
textmate-501.sock
zeb_def_ipc_3503
zeb_def_ipc_7450
exit: 0
./fence --settings bundled-binaries/config-deny-python3.json -c "bundled-binaries/run-python3.sh" 2>&1; echo "exit: $?"
bundled-binaries/run-python3.sh: line 2: /Users/dwt/Library/Caches/uv/archive-v0/TvtNho9_JQhr2BYzP0HOg/bin/python3: Operation not permitted
exit: 126

The runtime exec-deny fires at the OS level: the sandbox blocks the python3 binary path directly (the preflight never sees the command). ls is completely unaffected because none of python3's shared names appear on the critical-commands list.

@dwt
Copy link
Collaborator Author

dwt commented Mar 24, 2026

We might want to always log if a multi call binary blocks essential shell tools, not only with --debug. What do you think?

@jy-tan
Copy link
Contributor

jy-tan commented Mar 25, 2026

Thanks for this! A few thoughts:

  • Yes, I think it's better to log if a multi-call binary blocks essential path, not just in debug mode. Your sample debug log makes sense, but for normal logs it might be better to keep it a little shorter, perhaps something like:
    [Fence] runtime exec deny skipped for /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/coreutils (requested: dd): shared binary also implements critical commands. To force blocking, add "allowBlockingCritical": true to your command config. To silence this warning, add "dd" to "acknowledgeSharedBinary".
    
  • What do you think about renaming acknowledgeSharedBinary to something like more self-explanatory, e.g. silenceSharedBinaryWarning?
  • Just to confirm, we'll also be detecting and handling other common packages like busybox and uutils-coreutils as well yeah?

@dwt
Copy link
Collaborator Author

dwt commented Mar 25, 2026

  • Yes, I think it's better to log if a multi-call binary blocks essential path, not just in debug mode. Your sample debug log makes sense, but for normal logs it might be better to keep it a little shorter, perhaps something like:
    [Fence] runtime exec deny skipped for /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/coreutils (requested: dd): shared binary also implements critical commands. To force blocking, add "allowBlockingCritical": true to your command config. To silence this warning, add "dd" to "acknowledgeSharedBinary".
    

Sure, this is per definition a first draft to talk about the approach, the implementation is always fluid.

  • What do you think about renaming acknowledgeSharedBinary to something like more self-explanatory, e.g. silenceSharedBinaryWarning?

👍

  • Just to confirm, we'll also be detecting and handling other common packages like busybox and uutils-coreutils as well yeah?

This should detect them all, because it defines a set of binaries that the user probably never wants to block. If we want acceptance tests for that, I could set those up using nix. I.e. those tests would create a small VM with that setup and check that the warning triggers successfully.

@dwt dwt force-pushed the fix-bundled-binaries-overblock branch from 136d530 to 55566e7 Compare March 25, 2026 15:31
@dwt
Copy link
Collaborator Author

dwt commented Mar 25, 2026

Applied your feedback, and fiddled a bit with the error messages. What do you think?

Fence: Smart Shared-Binary Exec-Deny

2026-03-24T18:43:40Z by Showboat 0.6.1

Background

On Nix and nix-darwin every coreutils command (ls, cat, dd, head, tail, ...) is a symlink
pointing at a single multicall binary. The same pattern appears with uutils-coreutils and busybox.

When fence resolves symlinks before building its runtime exec-deny list, blocking dd resolves to
that shared binary — which also implements ls, cat, and ~100 other commands. Without protection,
the sandbox environment breaks entirely.

This document verifies the fix: the new logic detects critical-command collisions and handles them
with three distinct behaviours, while still blocking non-critical shared binaries (e.g. python3) normally.

Versions under test

echo "system fence:" && fence --version 2>&1 | grep "Version:" && echo "local fence (this branch):" && ./fence --version 2>&1 | grep "Version:"
system fence:
  Version: 0.1.32
local fence (this branch):
  Version: dev

The shared-binary situation on this machine

dd and ls are both Nix coreutils symlinks. Verify they resolve to the same canonical binary:

echo "dd  -> $(which dd) -> $(readlink $(which dd))" && echo "ls  -> $(which ls) -> $(readlink $(which ls))" && echo && echo "canonical dd: $(realpath $(which dd))" && echo "canonical ls: $(realpath $(which ls))" && echo && echo "commands sharing that binary: $(ls $(dirname $(realpath $(which dd))) | wc -l | tr -d " ") (including the coreutils binary itself)"
dd  -> /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/dd -> coreutils
ls  -> /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/ls -> coreutils

canonical dd: /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/coreutils
canonical ls: /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/coreutils

commands sharing that binary: 107 (including the coreutils binary itself)

106 symlinks + 1 real binary, all sharing one inode. Any path fence adds to its exec-deny list that resolves to this binary will block all 106 commands at once.

The bug: system fence (v0.1.32) breaks ls when dd is denied

Config: {"command":{"deny":["dd"],"useDefaults":false}}

Without the fix, resolving dd → coreutils canonical path → that path is added to the OS exec-deny list → every one of the 106 other coreutils symlinks (ls, cat, head, tail, ...) is collaterally blocked.

fence --settings bundled-binaries/config-deny-dd.json -c "ls /tmp" 2>&1; echo "exit: $?"
/nix/store/s0psayl7zvkvwdcqc8fy1sbv8rlf1yq8-bash-5.3p9/bin/bash: line 1: /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/ls: Operation not permitted
exit: 126

The sandbox blocks ls (exit 126) because it resolves to the same coreutils binary that dd was asked to deny. The user never asked to block ls.

Fix: local fence (dev) detects the critical collision and skips the coreutils path

Same config. The new logic detects that the coreutils canonical path also implements ls, cat, head, tail, and ~100 other critical commands — and skips adding it to the exec-deny list. The standalone macOS /bin/dd (a separate binary) is still blocked.

./fence --settings bundled-binaries/config-deny-dd.json -c "ls /tmp" 2>&1; echo "exit: $?"
[fence:macos] runtime exec deny skipped for /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/coreutils (requested: dd): shared binary also implements critical commands [cat head tail +29 more, use --debug for full list]. To force blocking add "allowBlockingCritical": true to your command config. To silence this warning add "dd" to "silenceSharedBinaryWarning".
TemporaryDirectory.4u9OdH
TemporaryDirectory.fc2cHS
fence
powerlog
ssh_mux_github.com_22_git
textmate-501.sock
zeb_def_ipc_10035
zeb_def_ipc_13168
zeb_def_ipc_17591
zeb_def_ipc_26177
zeb_def_ipc_3716
zeb_def_ipc_48161
zeb_def_ipc_8423
exit: 0

ls runs cleanly. The diagnostic is always printed to stderr — --debug only expands the truncated collision list to show every collateral command name:

./fence --debug --settings bundled-binaries/config-deny-dd.json -c "ls /tmp" 2>&1 | grep "fence:macos"
[fence:macos] runtime exec deny skipped for /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/coreutils (requested: dd): shared binary also implements critical commands [cat head tail echo sort wc cut tr uniq [ basename cp date dirname env false id ls mkdir mktemp mv printf pwd readlink realpath rmdir tee test touch true uname whoami]. To force blocking add "allowBlockingCritical": true to your command config. To silence this warning add "dd" to "silenceSharedBinaryWarning".

Opt-in: allowBlockingCritical: true forces the block anyway

Config: {"command":{"deny":["dd"],"useDefaults":false,"allowBlockingCritical":true}}

The user has explicitly accepted the risk. The coreutils path is added to the deny list — ls is blocked, no diagnostic is emitted.

./fence --settings bundled-binaries/config-deny-dd-force.json -c "ls /tmp" 2>&1; echo "exit: $?"
/nix/store/s0psayl7zvkvwdcqc8fy1sbv8rlf1yq8-bash-5.3p9/bin/bash: line 1: /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/ls: Operation not permitted
exit: 126

Opt-out: silenceSharedBinaryWarning: ["dd"] silences the warning

Config: {"command":{"deny":["dd"],"useDefaults":false,"silenceSharedBinaryWarning":["dd"]}}

The user has investigated, knows dd cannot be isolated on this system, and wants no noise about it. The coreutils path is silently skipped — ls works, and --debug logs nothing for this path.

./fence --settings bundled-binaries/config-deny-dd-ack.json -c "ls /tmp" 2>&1; echo "exit: $?"
TemporaryDirectory.4u9OdH
TemporaryDirectory.fc2cHS
fence
powerlog
ssh_mux_github.com_22_git
textmate-501.sock
zeb_def_ipc_10035
zeb_def_ipc_13168
zeb_def_ipc_17591
zeb_def_ipc_26177
zeb_def_ipc_3716
zeb_def_ipc_48161
zeb_def_ipc_8423
exit: 0
./fence --debug --settings bundled-binaries/config-deny-dd-ack.json -c "ls /tmp" 2>&1 | grep "fence:macos" || echo "(no diagnostic logged)"
(no diagnostic logged)

Non-critical shared binary: python3 is still blocked

Config: {"command":{"deny":["python3"],"useDefaults":false}}

python3, python3.13, and python3-config share a single binary on this machine — but none of those names are on the critical-commands list. There is no collateral damage to worry about, so the runtime exec-deny proceeds normally. ls is completely unaffected.

./fence --settings bundled-binaries/config-deny-python3.json -c "ls /tmp" 2>&1; echo "exit: $?"
TemporaryDirectory.4u9OdH
TemporaryDirectory.fc2cHS
fence
powerlog
ssh_mux_github.com_22_git
textmate-501.sock
zeb_def_ipc_10035
zeb_def_ipc_13168
zeb_def_ipc_17591
zeb_def_ipc_26177
zeb_def_ipc_3716
zeb_def_ipc_48161
zeb_def_ipc_8423
exit: 0
./fence --settings bundled-binaries/config-deny-python3.json -c "bundled-binaries/run-python3.sh" 2>&1; echo "exit: $?"
bundled-binaries/run-python3.sh: line 2: /Users/dwt/Library/Caches/uv/archive-v0/TvtNho9_JQhr2BYzP0HOg/bin/python3: Operation not permitted
exit: 126

The runtime exec-deny fires at the OS level: the sandbox blocks the python3 binary path directly (the preflight never sees the command). ls is completely unaffected because none of python3's shared names appear on the critical-commands list.

@dwt dwt force-pushed the fix-bundled-binaries-overblock branch from 55566e7 to 6923b15 Compare March 25, 2026 16:36
Copy link
Contributor

@jy-tan jy-tan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good!

@dwt dwt force-pushed the fix-bundled-binaries-overblock branch from 6923b15 to afa7ca9 Compare March 25, 2026 16:37
@jy-tan
Copy link
Contributor

jy-tan commented Mar 25, 2026

@dwt One thing, update docs/configuration.md and docs/linux-bwrap-mount-sequence.md to briefly mention the configuration options and exec deny nuances here.

@dwt
Copy link
Collaborator Author

dwt commented Mar 25, 2026

Some tradeoffs I'm thinking about: We could also log the potential problem and block anyway. That would be more secure, while still giving the user something to act on.

Detect shared executable targets by file identity and skip runtime path masking when a deny would block multiple command names, with debug diagnostics on Linux and macOS.

💘 Generated with Crush

Assisted-by: GPT-5.3 Codex via Crush <crush@charm.land>
@dwt dwt force-pushed the fix-bundled-binaries-overblock branch from afa7ca9 to 9abdc17 Compare March 25, 2026 19:15
@dwt
Copy link
Collaborator Author

dwt commented Mar 25, 2026

Thinking about this more, I really like the default block behavior far better. Fail loudly, make the user acknowledge with config and never silently poke holes into the sandbox.

@dwt
Copy link
Collaborator Author

dwt commented Mar 25, 2026

I've added the other implementation in a separate commit so we can easily revert to the first implementation. I like it quite a bit better as it doesn't break the sandbox promise that fence makes. Here's a demo:

Fence: Shared-Binary Exec-Deny — Block by Default with Actionable Warning

2026-03-25T19:46:59Z by Showboat 0.6.1

On Nix and nix-darwin every coreutils command (ls, cat, dd, head, tail, ...) is a symlink pointing at a single multicall binary. The same pattern appears with uutils-coreutils and busybox.

When fence resolves symlinks before building its runtime exec-deny list, blocking dd resolves to that shared binary — which also implements ls, cat, and ~100 other commands.

The previous behaviour silently skipped blocking the shared binary, emitting a warning to stderr that users might never see. The new behaviour blocks the binary by default and emits an actionable warning naming every collateral critical command. The sandbox is never silently weaker than what was configured. The user must explicitly opt out via acceptCannotRuntimeBlock if they accept that the command cannot be isolated at runtime on this system.

Version under test

./fence --version 2>&1 | grep Version:
  Version: dev

The shared-binary situation on this machine

dd and ls are both Nix coreutils symlinks. Verify they resolve to the same canonical binary:

sh -c 'echo "dd  -> $(which dd) -> $(readlink $(which dd))"; echo "ls  -> $(which ls) -> $(readlink $(which ls))"; echo; echo "canonical dd: $(realpath $(which dd))"; echo "canonical ls: $(realpath $(which ls))"; echo; echo "commands sharing that binary: $(ls $(dirname $(realpath $(which dd))) | wc -l | tr -d " ") (including the coreutils binary itself)"'
dd  -> /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/dd -> coreutils
ls  -> /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/ls -> coreutils

canonical dd: /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/coreutils
canonical ls: /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/coreutils

commands sharing that binary: 107 (including the coreutils binary itself)

106 symlinks + 1 real binary, all sharing one inode. Any path fence adds to its exec-deny list that resolves to this binary will block all 106 commands at once.

Scenario 1 — default behaviour: block with actionable warning

Config: {"command":{"deny":["dd"],"useDefaults":false}}

Fence detects the critical-command collision, blocks the shared binary anyway (the sandbox is never silently weakened), and emits a warning naming the collateral commands. ls is blocked as collateral damage — loudly, not silently:

./fence --settings bundled-binaries/config-deny-dd.json -c "ls bundled-binaries/example"; echo "exit: $?"
[fence:macos] runtime exec deny warning for /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/coreutils (requested: dd): shared binary also implements critical commands [cat head tail +29 more, use --debug for full list]. These commands will also be blocked. To accept this and silence the warning add "dd" to "acceptCannotRuntimeBlock" in your command config.
/nix/store/s0psayl7zvkvwdcqc8fy1sbv8rlf1yq8-bash-5.3p9/bin/bash: line 1: /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/ls: Operation not permitted
exit: 126

The warning is always printed to stderr. --debug expands the truncated collision list to every collateral command name:

./fence --debug --settings bundled-binaries/config-deny-dd.json -c "ls bundled-binaries/example" 2>&1 | grep "fence:macos"
[fence:macos] runtime exec deny warning for /nix/store/akih5l2yxpzqyh63xvyc6zsxl7kl2x4v-coreutils-9.10/bin/coreutils (requested: dd): shared binary also implements critical commands [cat head tail echo sort wc cut tr uniq [ basename cp date dirname env false id ls mkdir mktemp mv printf pwd readlink realpath rmdir tee test touch true uname whoami]. These commands will also be blocked. To accept this and silence the warning add "dd" to "acceptCannotRuntimeBlock" in your command config.

Scenario 2 — opt-out: acceptCannotRuntimeBlock: ["dd"]

Config: {"command":{"deny":["dd"],"useDefaults":false,"acceptCannotRuntimeBlock":["dd"]}}

The user has investigated, knows dd cannot be isolated on this system, and explicitly accepts that it cannot be runtime-blocked. The shared binary is silently skipped — ls works, and no diagnostic is logged even with --debug. The decision is recorded in the config for future auditors to see:

./fence --settings bundled-binaries/config-deny-dd-accept.json -c "ls bundled-binaries/example"; echo "exit: $?"
fnord
exit: 0
./fence --debug --settings bundled-binaries/config-deny-dd-accept.json -c "ls bundled-binaries/example" 2>&1 | grep "fence:macos" || echo "(no diagnostic logged)"
(no diagnostic logged)

Scenario 3 — non-critical shared binary: python3 is still blocked normally

Config: {"command":{"deny":["python3"],"useDefaults":false}}

python3, python3.13, and python3-config share a single binary on this machine — but none of those names are on the critical-commands list. There is no collateral damage to worry about, so the runtime exec-deny proceeds normally with no warning. ls is completely unaffected:

./fence --settings bundled-binaries/config-deny-python3.json -c "ls bundled-binaries/example"; echo "exit: $?"
fnord
exit: 0
./fence --settings bundled-binaries/config-deny-python3.json -c "bundled-binaries/run-python3.sh"; echo "exit: $?"
bundled-binaries/run-python3.sh: line 2: /Users/dwt/Library/Caches/uv/archive-v0/TvtNho9_JQhr2BYzP0HOg/bin/python3: Operation not permitted
exit: 126

The runtime exec-deny fires at the OS level: the sandbox blocks the python3 binary path directly. ls is completely unaffected because none of python3's shared names appear on the critical-commands list — no warning is emitted.

@dwt dwt force-pushed the fix-bundled-binaries-overblock branch from 8688f86 to c6380a5 Compare March 25, 2026 20:15
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="docs/schema/fence.schema.json">

<violation number="1" location="docs/schema/fence.schema.json:16">
P2: Adding `acceptCannotRuntimeBlock` without updating the docs leaves command-config guidance inconsistent with the schema and current runtime options.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@jy-tan
Copy link
Contributor

jy-tan commented Mar 25, 2026

This makes sense, on second thought I also agree with the security model here. A naming suggestion: skipSharedBinaryRuntimeDeny for the opt-out.

@dwt dwt force-pushed the fix-bundled-binaries-overblock branch from c6380a5 to fe052e8 Compare March 25, 2026 20:19
@dwt
Copy link
Collaborator Author

dwt commented Mar 25, 2026

This makes sense, on second thought I also agree with the security model here.

🎉

A naming suggestion: skipSharedBinaryRuntimeDeny for the opt-out.

Yeah that is better. What I liked about my name though is that it makes it very explicit that a security tradeoff was chosen. I am thinking about acceptSharedBinaryCannotRuntimeDeny.

@jy-tan
Copy link
Contributor

jy-tan commented Mar 25, 2026

Let's do ahead with that 👍

@dwt dwt force-pushed the fix-bundled-binaries-overblock branch 2 times, most recently from e0d2b1c to a396ae5 Compare March 25, 2026 20:43
…reak the shell environment

This gives the user an actionable warning to fix the problem, while not
silently weakening the sandbox.
@dwt dwt force-pushed the fix-bundled-binaries-overblock branch from a396ae5 to ee3b753 Compare March 25, 2026 20:46
@dwt
Copy link
Collaborator Author

dwt commented Mar 25, 2026

All done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants