Skip to content

fix(sandbox): deny WASM fs_write to the audit anchor via a capability deny-list#6196

Merged
houko merged 6 commits into
mainfrom
feat/6182-audit-anchor-write-denylist
Jun 19, 2026
Merged

fix(sandbox): deny WASM fs_write to the audit anchor via a capability deny-list#6196
houko merged 6 commits into
mainfrom
feat/6182-audit-anchor-write-denylist

Conversation

@houko

@houko houko commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #6182. The WASM skill sandbox gated fs_write solely on glob capability matching. A skill manifest granting a broad FileWrite subtree — or the universal FileWrite("*") — passed check_capability against the audit anchor and could truncate it, silently breaking the tamper-evident audit Merkle chain. host_fs_write had no knowledge of the anchor path (resolved only kernel-side at boot).

SECURITY.md documents that the anchor's integrity rests on filesystem permissions; this adds a sandbox-level deny-list so a capability-bounded skill cannot reach it regardless of grant.

Changes

  • crates/librefang-kernel-handle/src/tool_policy.rs — new ToolPolicy::protected_write_paths() trait method (default vec![], so stub kernels are unaffected).
  • crates/librefang-kernel/src/kernel/handles/tool_policy.rs — kernel impl returns the boot-resolved audit anchor ([audit].anchor_path, default data_dir/audit.anchor), resolving relative paths against data_dir in lockstep with boot.rs (absolute wins / relative joins data_dir / unset defaults).
  • crates/librefang-runtime/src/host_functions.rshost_fs_write denies any write whose canonical target matches a protected path, above the capability check (defense in depth). New is_protected_write_target helper canonicalizes protected paths the same way the write target is resolved (safe_resolve_parent), so symlink resolution and macOS /private aliasing match on both sides. The deny-list is scoped strictly to the anchor file, not all of data_dir.
  • SECURITY.md — documents the new sandbox-level protection under the anchor threat model.
  • CHANGELOG.md — entry under [Unreleased] → Added.

Verification

cargo check --workspace --lib                                              # ✓
cargo clippy -p librefang-kernel-handle -p librefang-runtime -p librefang-kernel --lib -- -D warnings  # ✓
cargo test -p librefang-runtime --lib fs_write                             # 5 passed
cargo fmt --check                                                          # clean

New tests in host_functions.rs:

  • test_fs_write_denied_protected_audit_anchor_even_with_wildcardFileWrite("*") cannot truncate the anchor; content stays unchanged.
  • test_fs_write_allows_non_protected_sibling_with_wildcard — a sibling path in the same dir still writes (deny-list isn't a data_dir-wide ban).
  • test_fs_write_no_kernel_skips_protected_check — a kernel-less guest skips the check.

Duplicate

#6181 has an identical title, author, and proposed fix (its body carries unrelated copy-paste noise). This PR tracks the work under #6182; I recommend the maintainer close #6181 as a duplicate — I have not closed it.

@github-actions github-actions Bot added the size/M 50-249 lines changed label Jun 18, 2026
@github-actions github-actions Bot added has-conflicts PR has merge conflicts that need resolution area/docs Documentation and guides area/runtime Agent loop, LLM drivers, WASM sandbox area/kernel Core kernel (scheduling, RBAC, workflows) area/security Security systems and auditing labels Jun 18, 2026
Evan and others added 2 commits June 18, 2026 16:37
… deny-list

The WASM sandbox gated fs_write solely on glob capability matching, so a skill
granted a broad FileWrite subtree -- or the universal FileWrite("*") -- could
truncate the audit anchor ([audit].anchor_path, default data_dir/audit.anchor)
and silently break the tamper-evident Merkle chain. host_fs_write had no
knowledge of protected paths.

Add a ToolPolicy::protected_write_paths() method (default empty; the kernel
returns the boot-resolved anchor, resolving relative paths against data_dir in
lockstep with boot.rs). host_fs_write now denies any write whose canonical
target matches a protected path ABOVE the capability check, as defense in
depth, so even FileWrite("*") cannot reach the anchor. The deny-list is
scoped strictly to the anchor file, not all of data_dir, to keep the blast
radius small. Protected paths are canonicalized the same way the write target
is resolved, so symlink and macOS /private aliasing match.

Tests: FileWrite("*") cannot truncate the anchor (content unchanged); a
sibling path under the same dir still writes; a kernel-less guest skips the
check. SECURITY.md updated to document the new sandbox-level protection.

Closes #6182. Supersedes the duplicate #6181.
…nvention

CLAUDE.md forbids multi-paragraph docstrings and multi-line comment blocks
(one short line max) and prohibits referencing the originating issue number
inside inline code comments (those belong in PR/CHANGELOG, not source).

- Collapse 13-line `protected_write_paths` trait doc to one line
- Collapse 9-line `is_protected_write_target` doc to one line
- Collapse 8-line kernel impl comment to one line
- Replace 7-line `// SECURITY (#6182):` block with single-line note
- Drop 2-3 line test-body comments whose content the function name conveys
- Remove `protected_paths` test-struct field doc (name is self-explanatory)
@houko
houko force-pushed the feat/6182-audit-anchor-write-denylist branch from 6b57fcc to 102e01d Compare June 18, 2026 07:37
@github-actions github-actions Bot added ready-for-review PR is ready for maintainer review and removed has-conflicts PR has merge conflicts that need resolution labels Jun 18, 2026
@houko houko changed the title sec(sandbox): deny WASM fs_write to the audit anchor via a capability deny-list fix(sandbox): deny WASM fs_write to the audit anchor via a capability deny-list Jun 18, 2026
@github-actions github-actions Bot added has-conflicts PR has merge conflicts that need resolution and removed ready-for-review PR is ready for maintainer review labels Jun 19, 2026
Exact PathBuf comparison in is_protected_write_target left a bypass on
case-insensitive filesystems (macOS APFS default, NTFS): a guest could
target .../AUDIT.ANCHOR and the OS would still truncate the on-disk
.../audit.anchor, slipping past the deny-list.
Once the canonical parents match, compare the leaf case-insensitively so
case-variants of the protected filename are refused.
Adds a regression test exercising the AUDIT.ANCHOR variant.
@houko
houko enabled auto-merge (squash) June 19, 2026 04:50
@cloudflare-workers-and-pages

Copy link
Copy Markdown

@github-actions github-actions Bot added ready-for-review PR is ready for maintainer review and removed has-conflicts PR has merge conflicts that need resolution labels Jun 19, 2026
@houko
houko merged commit 7755b52 into main Jun 19, 2026
32 checks passed
@houko
houko deleted the feat/6182-audit-anchor-write-denylist branch June 19, 2026 07:00
GQAdonis pushed a commit to GQAdonis/librefang that referenced this pull request Jun 19, 2026
Merge upstream/main into the BossFang fork; origin/main was 15 commits behind.

Notable upstream changes:
- librefang#6225 scope the compaction-summary banner to the compacted session (adds canonical_sessions.compacted_summary_session_id).
- librefang#6196 deny WASM fs_write to the audit anchor via a capability deny-list.
- librefang#6226 / librefang#6227 add agent label and exit-reason metrics to the agent loop.
- librefang#6215 context-window usage indicator + honest quota-error classification.
- librefang#6217 per-instance sidecar secrets so each agent owns its own handle.
- librefang#6194 global Auto-Dream on/off switch on the Memory tab.
- librefang#6211 / librefang#6214 / librefang#6212 token/context cap fixes; librefang#6208 refuse to delete the active prompt version.
- librefang#6218 browser_tools.rs ToolError migration; librefang#6203 launchctl let-binding build fix; librefang#6224 docs dep bumps; librefang#6193 drop five orphaned email deps.

Conflict resolution:
- deny.toml: keep our RUSTSEC-2025-0141 (bincode 2.0.x) ignore entry (take ours).
- deny.toml: ignore three pre-existing unmaintained advisories newly tripped by the RustSec DB (all transitive via the UAR provider chain, none introduced by this merge): RUSTSEC-2024-0384 (instant), RUSTSEC-2024-0436 (paste), RUSTSEC-2025-0119 (number_prefix).

BossFang preservation (SurrealDB schema parity for upstream SQLite v46 / librefang#6225):
- Add crates/librefang-storage/src/migrations/sql/032_canonical_sessions_compacted_summary_session_id.surql declaring the new field on the SCHEMAFULL canonical_sessions table (SCHEMAFULL silently drops undefined fields on write), registered as version 32 in migrations/mod.rs.
- migrate/sqlite_to_surreal.rs: copy the new column so a SQLite to SurrealDB migration preserves the owning-session pointer rather than dropping it.
- backends/surreal_session.rs: preserve compacted_summary_session_id across canonical appends, since the upsert replaces the whole record.
- Cargo.lock: pick up the workspace version bump (beta.19 to beta.20) the merge introduced.

Verification:
- cargo check --workspace --lib — clean.
- cargo check -p librefang-storage -p librefang-memory -p librefang-uar-spec — clean.
- cargo test -p librefang-storage migration — ok (migration ordering / SurrealDB-3 flexible-syntax invariants).
- cargo test -p librefang-memory --lib session — 50 passed (incl. upstream's store_llm_summary round-trip test).
- cargo clippy -p librefang-storage -p librefang-memory — clean.
- python3 scripts/enforce-branding.py --check — clean; Tauri desktop audit and URL-drift scan both clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Documentation and guides area/kernel Core kernel (scheduling, RBAC, workflows) area/runtime Agent loop, LLM drivers, WASM sandbox area/security Security systems and auditing ready-for-review PR is ready for maintainer review size/M 50-249 lines changed

Projects

None yet

2 participants