Skip to content

fix(builtins): surface readonly errors from unset/declare/export#1553

Merged
chaliy merged 1 commit intomainfrom
claude/threat-model-issue-IfuVb
May 6, 2026
Merged

fix(builtins): surface readonly errors from unset/declare/export#1553
chaliy merged 1 commit intomainfrom
claude/threat-model-issue-IfuVb

Conversation

@chaliy
Copy link
Copy Markdown
Contributor

@chaliy chaliy commented May 6, 2026

Summary

Closes the integrity-bypass gap in three threats from specs/threat-model.md §4. All three builtins were silently skipping the operation when the target was a readonly variable, leaving the caller unable to distinguish a refused write from a success. Real bash prints bash: <op>: <name>: readonly variable on stderr and exits 1; we now match.

Mitigates:

  • TM-INJ-019unset on readonly variable
  • TM-INJ-020declare X=new over readonly X=old
  • TM-INJ-021export X=new over readonly X=old

Why

The threat model already documented the gap as OPEN with the recommendation "Check readonly attribute in ". The check was actually present (the value never got overwritten), but the result was a silent skip — continue without stderr or exit-code change. A script that does export TOKEN=new || die "couldn't rotate" would happily proceed with the old value and never see the failure. Fixing this also matches Bash semantics, so spec-compatibility benefits.

How

  • crates/bashkit/src/builtins/export.rs: accumulate stderr and exit_code = 1 when refusing an overwrite; return them on the ExecResult.
  • crates/bashkit/src/interpreter/mod.rs:
    • declare assignment loop: same accumulator pattern; the final ExecResult carries the error state.
    • execute_unset_builtin: was a separate code path from builtins/vars.rs::Unset and was silently skipping both readonly variables and internal markers; now both paths report the standard message and exit 1. (builtins/vars.rs::Unset was already correct.)
  • Remaining args after a refused write keep being processed (Bash behavior) — the new declare_continues_after_readonly_error test guards this.
  • specs/threat-model.md: §4 status flipped to MITIGATED with the actual mitigation text; "Open (Blackbox)" table strikes through the three rows.

Tests

New tests in crates/bashkit/tests/blackbox_security_tests.rs::finding_readonly_bypass:

  • unset_readonly_reports_error_and_exit_1
  • declare_readonly_reports_error_and_exit_1
  • export_readonly_reports_error_and_exit_1
  • declare_continues_after_readonly_error — covers the multi-arg case

The existing tests in the same module (which already verified value preservation) continue to pass unchanged.

Verified locally:

  • cargo test -p bashkit --lib: 2207 pass.
  • cargo test -p bashkit --test blackbox_security_tests: 82 pass (10 in the readonly module).
  • cargo test -p bashkit --test threat_model_tests: 170 pass (1 pre-existing unrelated failure on main: builtin_parser_depth::threat_jq_moderate_nesting_works).
  • cargo test -p bashkit --test spec_tests: all green.
  • cargo fmt --check, cargo clippy -p bashkit --all-targets -- -D warnings: clean.

Test plan

  • Stricter readonly tests (exit code + stderr) for unset, declare, export
  • Multi-arg declare still processes good args after a readonly hit
  • Existing value-preservation tests unchanged
  • No regression in spec or threat-model suites
  • fmt + clippy clean

Generated by Claude Code

Closes the integrity-bypass gap in TM-INJ-019/020/021: all three builtins
were silently skipping the operation when the target was readonly, so a
caller could not distinguish a refused write from a successful one. Real
bash prints "bash: <op>: <name>: readonly variable" on stderr and exits 1.

Mitigates:
- TM-INJ-019: unset on readonly variable
- TM-INJ-020: declare X=new over readonly X=old
- TM-INJ-021: export X=new over readonly X=old

The interpreter's execute_unset_builtin path was also silently skipping
(separate from builtins/vars.rs::Unset which already reported correctly);
both paths now match. Remaining args after a refused write keep being
processed, matching bash.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
bashkit 4ae00f6 Commit Preview URL

Branch Preview URL
May 06 2026, 04:52 AM

@chaliy chaliy merged commit ee9b579 into main May 6, 2026
34 checks passed
@chaliy chaliy deleted the claude/threat-model-issue-IfuVb branch May 6, 2026 05:05
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.

1 participant