fix: skip runtime exec deny for shared executable targets#70
fix: skip runtime exec deny for shared executable targets#70dwt wants to merge 2 commits intoUse-Tusk:mainfrom
Conversation
e8c5d3e to
136d530
Compare
Fence: Smart Shared-Binary Exec-Deny2026-03-24T18:43:40Z by Showboat 0.6.1 BackgroundOn Nix and nix-darwin every coreutils command ( When fence resolves symlinks before building its runtime exec-deny list, blocking This document verifies the fix: the new logic detects critical-command collisions and handles them Versions under testecho "system fence:" && fence --version 2>&1 | grep "Version:" && echo "local fence (this branch):" && ./fence --version 2>&1 | grep "Version:"The shared-binary situation on this machine
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)"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
|
|
We might want to always log if a multi call binary blocks essential shell tools, not only with |
|
Thanks for this! A few thoughts:
|
Sure, this is per definition a first draft to talk about the approach, the implementation is always fluid.
👍
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. |
136d530 to
55566e7
Compare
|
Applied your feedback, and fiddled a bit with the error messages. What do you think? Fence: Smart Shared-Binary Exec-Deny2026-03-24T18:43:40Z by Showboat 0.6.1 BackgroundOn Nix and nix-darwin every coreutils command ( When fence resolves symlinks before building its runtime exec-deny list, blocking This document verifies the fix: the new logic detects critical-command collisions and handles them Versions under testecho "system fence:" && fence --version 2>&1 | grep "Version:" && echo "local fence (this branch):" && ./fence --version 2>&1 | grep "Version:"The shared-binary situation on this machine
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)"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
|
55566e7 to
6923b15
Compare
6923b15 to
afa7ca9
Compare
|
@dwt One thing, update |
|
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>
afa7ca9 to
9abdc17
Compare
|
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. |
|
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 Warning2026-03-25T19:46:59Z by Showboat 0.6.1 On Nix and nix-darwin every coreutils command ( When fence resolves symlinks before building its runtime exec-deny list, blocking 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 Version under test./fence --version 2>&1 | grep Version:The shared-binary situation on this machine
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)"'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 warningConfig: 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. ./fence --settings bundled-binaries/config-deny-dd.json -c "ls bundled-binaries/example"; echo "exit: $?"The warning is always printed to stderr. ./fence --debug --settings bundled-binaries/config-deny-dd.json -c "ls bundled-binaries/example" 2>&1 | grep "fence:macos"Scenario 2 — opt-out:
|
8688f86 to
c6380a5
Compare
There was a problem hiding this comment.
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.
|
This makes sense, on second thought I also agree with the security model here. A naming suggestion: |
c6380a5 to
fe052e8
Compare
🎉
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 |
|
Let's do ahead with that 👍 |
e0d2b1c to
a396ae5
Compare
…reak the shell environment This gives the user an actionable warning to fix the problem, while not silently weakening the sandbox.
a396ae5 to
ee3b753
Compare
|
All done. |
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,ddis a symlink to it, but so doesls,cat, … So blockingcoreutilsbecauseddresolves to it is not a good idea.Closes: #67