Context
Came up while investigating fs-deny gaps for #66 (the docker.sock defense-in-depth fix in #84 made me look at ProtectedPaths, which learn mode wipes entirely). Learn mode (--learn) is a legitimate, intentional escape hatch for bootstrapping a sandbox profile: run once with filesystem restrictions fully lifted, observe which folders get touched (internal/sandboxrun/learn.go), then prompt the human to add the observed folders to filesystem.allow for future restricted runs. Network and env filtering stay active throughout (internal/sandboxrun/run.go:63-66) — only the filesystem is opened (withUnrestrictedFilesystem, internal/sandboxrun/grants.go:425-434: AllowPaths gets /, ProtectedPaths is set to nil).
The mechanism itself is reasonable (contained, human-gated). The gaps are in what surrounds it:
Gaps
Verified, not a gap (closing the open question from investigation)
While investigating this I flagged a question: could an agent already running inside a normal restricted sandbox re-invoke omac sandbox run --learn (or omac serve --learn) itself and thereby escalate its own access? Tested empirically with bubblewrap directly (not via omac, to isolate the kernel-level mechanism):
outer bwrap (restricted, secret_dir NOT bound in)
-> ls secret_dir: "No such file or directory" (as expected)
-> nested bwrap --bind / / (equivalent to withUnrestrictedFilesystem's AllowPaths=["/"])
-> ls secret_dir from inside the nested bwrap: still "No such file or directory"
A new mount namespace is always built from a copy of the current mount table, not the true host root — so binding / inside an already-restricted namespace just re-binds the restricted view onto itself. This confirms self-reinvocation of --learn from inside an existing sandboxed session cannot escalate filesystem visibility on Linux/bubblewrap. (Not independently verified on macOS/Seatbelt, which is a different, policy-based mechanism rather than namespace-based, but Seatbelt profiles are similarly monotonic — a sandboxed process cannot broaden its own applied profile without root.) No action needed here beyond noting it, unless someone wants to add a regression test pinning this behavior.
Suggested scope
Probably two follow-ups, not one PR: (1) add a dedicated audit-log entry for learn-mode sessions (start/stop, maybe periodic sampled paths, not just the final approved list) + a README/docs section explaining the trust model, and (2) an e2e test exercising --learn against a real sandbox. Filing as one issue since they're all "learn mode is under-observed" — happy to split once someone picks it up.
Context
Came up while investigating fs-deny gaps for #66 (the docker.sock defense-in-depth fix in #84 made me look at
ProtectedPaths, which learn mode wipes entirely). Learn mode (--learn) is a legitimate, intentional escape hatch for bootstrapping a sandbox profile: run once with filesystem restrictions fully lifted, observe which folders get touched (internal/sandboxrun/learn.go), then prompt the human to add the observed folders tofilesystem.allowfor future restricted runs. Network and env filtering stay active throughout (internal/sandboxrun/run.go:63-66) — only the filesystem is opened (withUnrestrictedFilesystem,internal/sandboxrun/grants.go:425-434:AllowPathsgets/,ProtectedPathsis set tonil).The mechanism itself is reasonable (contained, human-gated). The gaps are in what surrounds it:
Gaps
learnRecorder(internal/sandboxrun/learn.go) samples open file descriptors in memory every 2s (learnSampleInterval,learn.go:18) and, at exit, only the final aggregated/human-approved candidate folder list gets persisted (into the profile, only if the user answersyto theAdd them to filesystem.allow?prompt). If something bad happens during an unrestricted session — or the user answersN— there's no record anywhere of what was actually opened. Given the session had full host filesystem access for that duration, this is the one window where an audit log matters most and is currently the one place it's missing.--helpstring onomac serve --learn(internal/cli/serve.go:58) and a runtime stderr banner (internal/sandboxrun/run.go:69). The lower-levelomac sandbox run --learnaccepts the flag (internal/sandboxprofile/flags.go:170-174) but doesn't even show it in that command's own--helpusage text (internal/cli/sandbox_cmd.go:55-78). A user could stumble into "full host filesystem access, unrestricted" without any written guidance on when that's appropriate.internal/sandboxrun/learn_test.gocovers the pure aggregation/exclusion/prompt logic against hand-builtGrantsvalues, but nothing launches a real kernel sandbox (bubblewrap/Seatbelt) with--learnand verifies the filesystem is actually opened as expected end-to-end, or that network/env filtering genuinely stay enforced during a learn-mode session.internal/e2e/e2e_test.gohas zero references to learn mode.Verified, not a gap (closing the open question from investigation)
While investigating this I flagged a question: could an agent already running inside a normal restricted sandbox re-invoke
omac sandbox run --learn(oromac serve --learn) itself and thereby escalate its own access? Tested empirically with bubblewrap directly (not via omac, to isolate the kernel-level mechanism):A new mount namespace is always built from a copy of the current mount table, not the true host root — so binding
/inside an already-restricted namespace just re-binds the restricted view onto itself. This confirms self-reinvocation of--learnfrom inside an existing sandboxed session cannot escalate filesystem visibility on Linux/bubblewrap. (Not independently verified on macOS/Seatbelt, which is a different, policy-based mechanism rather than namespace-based, but Seatbelt profiles are similarly monotonic — a sandboxed process cannot broaden its own applied profile without root.) No action needed here beyond noting it, unless someone wants to add a regression test pinning this behavior.Suggested scope
Probably two follow-ups, not one PR: (1) add a dedicated audit-log entry for learn-mode sessions (start/stop, maybe periodic sampled paths, not just the final approved list) + a README/docs section explaining the trust model, and (2) an e2e test exercising
--learnagainst a real sandbox. Filing as one issue since they're all "learn mode is under-observed" — happy to split once someone picks it up.