Skip to content

fix(safety): compile baked policy to code to resist binary tampering#540

Merged
steipete merged 1 commit intoopenclaw:mainfrom
drewburchfield:fix/safety-profile-hash-policy
May 4, 2026
Merged

fix(safety): compile baked policy to code to resist binary tampering#540
steipete merged 1 commit intoopenclaw:mainfrom
drewburchfield:fix/safety-profile-hash-policy

Conversation

@drewburchfield
Copy link
Copy Markdown
Contributor

@drewburchfield drewburchfield commented Apr 30, 2026

Summary

  • compile the baked policy to FNV-64a hash switches at build time so rule strings no longer appear in the binary
  • move the YAML parser, hash function, and codegen IR to a new internal/safetyprofile/ package; stock cmd/gog no longer imports gopkg.in/yaml.v3 and the parser is unreachable at runtime
  • reject deny: [all] / deny: ["*"] and nesting under a wildcard prefix at parse time (their hashes never matched real command paths); reject non-string or empty name: (was silently accepted as unnamed)

This hardens #536 against an in-place sed patch on the binary. Same profile YAML, same build-safe.sh, same runtime contract; loadBakedSafetyProfile keeps its (profile, error) signature so help_printer.go and schema.go are untouched.

Verification

  • make test
  • make lint
  • make ci
  • ./build-safe.sh safety-profiles/agent-safe.yaml -o bin/gog-agent-safe-test
  • ./build-safe.sh safety-profiles/readonly.yaml -o bin/gog-readonly-test
  • ./build-safe.sh safety-profiles/full.yaml -o bin/gog-full-test
  • ./bin/gog-agent-safe-test gmail drafts send draft-1 exits 2 with baked profile block
  • strings bin/gog-agent-safe-test | grep -c '^gmail.send$'0
  • codex exec review --base upstream/main → no findings

The baked profile YAML lives in the binary as a string literal. With
write access to the binary, an attacker can locate it via `strings` and
flip rule bytes in place with `sed` (e.g. "send: false" -> "send: true ")
without rebuilding, weakening the doc's claim that the policy "cannot
be changed with flags, environment variables, config files, or shell
arguments".

cmd/bake-safety-profile parses the profile YAML at build time and emits
a Go file (//go:build safety_profile) of switch statements over FNV-64a
hashes of dotted command paths. Rule strings like "gmail.send" no longer
appear in the binary; the only embedded string is the profile name
(used in error messages). Patching a blocked command back on now
requires editing compiled machine code, not flipping ASCII bytes.

Layout:

- internal/safetyprofile/ is a new build-time-only package containing
  the YAML parser, the FNV-64a hash function, and the codegen IR. It is
  imported by cmd/bake-safety-profile and the safety_profile-tagged
  hash-agreement test, but not by cmd/gog. Stock builds therefore drop
  gopkg.in/yaml.v3 entirely (~460 KB smaller binary) and the parser
  surface is no longer reachable at runtime.

- internal/cmd/safety_profile.go keeps only the runtime contract:
  the bakedSafetyProfile handle, enforcement, help/schema visibility,
  and a small bakedSafetyHashPath helper that the generated switch
  calls. loadBakedSafetyProfile keeps its (profile, error) signature
  so help_printer.go and schema.go are untouched; the error is now
  always nil because validation moved to build time.

- internal/cmd/safety_profile_default.go (//go:build !safety_profile)
  exposes a test-only override that withBakedSafetyProfile mutates to
  set up scenarios. Stock binaries leave it zeroed and the profile
  reports disabled.

- cmd/bake-safety-profile/safety_profile_baked_gen.go is the generated
  output (//go:build safety_profile). The header carries the profile
  name, rule counts, and FNV-64a algorithm note. No rule names or
  hashes appear in the comment; profile names with newline or carriage
  return characters are sanitized so they cannot break out of the
  comment line.

The parser also rejects three malformed shapes that would otherwise be
silent no-ops in the hashed runtime switch:

- `deny: [all]` / `deny: ["*"]`: deny-side wildcards never match a real
  command path through the hashed dispatch, so they reject at parse
  time with a clear error.
- `all: { gmail: false }` / `"*": { gmail: false }`: nesting under a
  wildcard prefix is rejected for the same reason.
- `name:` non-string or empty: rejected before the rest of the parse so
  the error points at the malformed field.

Stock build behavior is preserved. Profile YAML format, build-safe.sh,
runtime contract (allow/deny semantics, help filtering, schema
filtering), end-user error messages, and the upstream callsites in
help_printer.go and schema.go are all unchanged. The new validation
only fires when authoring a malformed profile.

TestSafetyProfileHashAgreement asserts the runtime
bakedSafetyHashPath and build-time safetyprofile.HashRule produce
identical values for the command paths the generator emits switches
over.
Copy link
Copy Markdown
Collaborator

@steipete steipete left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the generated hash-switch path, parser split, runtime enforcement, help/schema filtering contract, and baked build flow.

Local verification:

  • go test ./cmd/bake-safety-profile ./internal/safetyprofile ./internal/cmd
  • make lint
  • ./build-safe.sh safety-profiles/agent-safe.yaml -o bin/gog-agent-safe-review
  • ./build-safe.sh safety-profiles/readonly.yaml -o bin/gog-readonly-review
  • ./bin/gog-agent-safe-review gmail drafts send draft-1 exits 2 with the baked profile block
  • ./bin/gog-readonly-review calendar update primary ev --summary x exits 2 with the readonly baked profile block
  • strings bin/gog-readonly-review has no raw calendar.update, gmail.send, or gmail.drafts.send rule strings

No blocking findings.

@steipete steipete merged commit 4690010 into openclaw:main May 4, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants