Skip to content

Learn mode: no audit trail, no docs, no e2e coverage #85

Description

@nhuelstng

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

  • No audit trail of raw filesystem activity. 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 answers y to the Add them to filesystem.allow? prompt). If something bad happens during an unrestricted session — or the user answers N — 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.
  • No user-facing documentation. Zero mentions in README.md/docs/AGENTS.md. The only explanation is a --help string on omac serve --learn (internal/cli/serve.go:58) and a runtime stderr banner (internal/sandboxrun/run.go:69). The lower-level omac sandbox run --learn accepts the flag (internal/sandboxprofile/flags.go:170-174) but doesn't even show it in that command's own --help usage 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.
  • No e2e coverage. internal/sandboxrun/learn_test.go covers the pure aggregation/exclusion/prompt logic against hand-built Grants values, but nothing launches a real kernel sandbox (bubblewrap/Seatbelt) with --learn and 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.go has 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 (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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions