DEV Community

Hassan Balbakie
Hassan Balbakie

Posted on

husk: I extracted the sandbox from my last project, and made the tests do the talking

Where this came from

A few weeks ago I shipped secfix — a tool that validates vulnerability scanner findings by actually executing the flagged code instead of guessing from the source. Semgrep finds something, secfix builds a harness with a tainted sentinel, runs it, and checks the execution trace to see if the "vulnerability" is real.

That "run it somewhere it can't hurt me" piece was one Python module inside secfix: a locked-down Docker sandbox. It occurred to me that this piece was useful far beyond vuln validation — anyone running untrusted or AI-generated code needs exactly this, and most people either skip it or hand-roll something they never actually test.

So I pulled it out, generalized it, and gave it one job: detonate untrusted Python safely, and prove it.

What husk is
python
from husk import detonate

result = detonate("print('hello from inside the sandbox')")
print(result.stdout) # hello from inside the sandbox
print(result.exit_code) # 0
print(result.timed_out) # False

One function. Give it code (or a path to a script), get back stdout, stderr, exit code, whether it timed out, and how long it took. A thin CLI wraps the same thing:

husk run script.py --timeout 10

Under the hood, every run is --network none, non-root, --read-only rootfs with a small --tmpfs /tmp, --cap-drop=ALL, --security-opt=no-new-privileges, capped CPU/memory/PIDs, --rm, and a host-side timeout that kills the container rather than trusting it to terminate itself.

None of that is novel. What I actually wanted to fix is more boring, and more important: most sandboxes never prove their own claims.

The part that matters: adversarial tests, not a claims list

It's easy to write a README that says "no network access" and never check whether a script running inside the container can actually get out. That gap is the whole ballgame — a security tool whose guarantees are untested is just marketing copy with extra steps.

So before I wrote the runner, I wrote the attacks:

attempt_network.py — tries three different outbound connections
attempt_escape_fs.py — tries to write outside /tmp
attempt_fork_bomb.py — bursts processes in a loop
attempt_privesc.py — reaches for a capability-gated syscall
run_forever.py — infinite loop, to prove the timeout actually fires
benign.py — a normal script, the control case that has to succeed

Then tests/test_isolation.py runs each one through detonate() and checks the specific failure mode, not just "something went wrong":

The network test doesn't just check for a nonzero exit — it asserts the stderr shows a genuine connection-level failure (Network is unreachable, a gaierror, a URLError). A broken import accidentally "blocking" the network shouldn't be able to pass this test.
The filesystem test asserts Read-only file system in stderr and that the write inside /tmp succeeded — read-only isolation shouldn't mean nothing works at all.
The fork-bomb test parses the fixture's own summary line and asserts blocked > 0 and succeeded < attempted — throttled, not just crashed into something unrelated.
The timeout test asserts the container was killed promptly by the host, not left to hit some unrelated resource ceiling.

If any of these six tests fail, the tool doesn't ship. That's the actual gate — not "does it run hello world."

A design choice I want to call out: no bind mount, ever

Code goes into the container over stdin (docker run -i ... python3 -), not as a mounted file. There's no host path visible inside the container at all.

This wasn't about convenience. The usual question with sandboxes — "which host paths are exposed, and can the code escape through them?" — doesn't get answered here, it gets removed. There's no mount to escape from and no mount config to quietly misconfigure six months from now.

What husk explicitly does NOT protect against

This part goes in the README above the fold, not buried at the bottom, because I think it's the actually useful information:

It shares the host kernel. This is Docker — namespaces and cgroups, not a hypervisor boundary. It is not gVisor, Firecracker, or Kata, and makes no claim to be. If the code you're running might carry a working container-escape exploit, husk alone isn't enough.
No side-channel defense. Nothing here mitigates cache-timing or Spectre-class attacks — a shared kernel and shared cores don't allow it.
No dependency-install phase. v0.1.0 runs one self-contained script with the network fully off. A future "pip install these first" step needs network access and is its own supply-chain risk surface — one this tool doesn't cover today, and won't quietly start covering later without saying so loudly.
It needs local Docker socket access, which is real friction inside a restricted or rootless CI environment.
Try it / read the proof
git clone https://github.com/balbaks/husk
cd husk
pip install -e .
husk run examples/benign.py

The claims above are only worth what the tests are worth, so don't take my word for it — read tests/test_isolation.py directly.

Repo: https://github.com/balbaks/husk

This is branch #2 in a small, ongoing series of honest security tools. Branch #1 was secfix, which refuses to call a vulnerability "fixed" unless a fresh execution trace proves it — and which documents in detail exactly where that approach hits a wall on real framework code (spoiler: inferring a Django view's full data dependencies is where solo-scope work ends and team-scale integration begins).

Same principle both times: say what's verified. Name what isn't.

Top comments (0)