You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: recurse into sub-expressions in is_safe_expect_expr (0.2.1) (#2)
* Recurse into expect expression validation and add regression tests
* Recurse into expect expressions to reject nested unsafe and blocks
* chore: clarify expect validation scope, add regression tests, bump to 0.2.1
- Update comment and error message in validate_local_test_expects to
accurately describe what is and isn't blocked: syntax-level injection
vectors (block, unsafe, closure, control-flow) are rejected; call
expressions to side-effectful functions are not blocked since .unit.spec
files are treated as trusted input (same as body.rust)
- Add 4 regression tests for Field, Index, Unary, and Cast recursive arms
to ensure sub-expression recursion is covered
- Track unbounded recursion finding in TODOS.md (no depth cap in
is_safe_expect_expr; low-urgency given trusted input model)
- Bump version 0.2.0 → 0.2.1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+10Lines changed: 10 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,5 +1,15 @@
1
1
# Changelog
2
2
3
+
## 0.2.1 - 2026-04-03
4
+
5
+
### Security
6
+
7
+
-**`is_safe_expect_expr` now recurses into sub-expressions** — Previously, the expression whitelist in `spec validate` only inspected the top-level AST node. A call like `f({ unsafe { ... } })` would pass because the outer `Call` was whitelisted without checking its arguments. All Arms (Binary, Call, MethodCall, Field, Index, Unary, Paren, Cast) now recurse into every sub-expression; `unsafe`, block, closure, and control-flow forms are rejected wherever they appear in the tree. Error message updated from "simple expression" framing to "block, unsafe, closure" framing to accurately describe what is and isn't blocked.
8
+
9
+
### Testing
10
+
11
+
- Added 4 regression tests covering recursion through Field, Index, Unary, and Cast arms.
-[ ]**Add `local_tests[].id` uniqueness validation** — Duplicate ids within a unit → duplicate `fn test_{id}()` → compile error. Validate uniqueness in `validate_local_test_expects`. (validator.rs)
59
59
-[ ]**Document `pub use generated::*` as required consuming-crate convention** — Internal deps generate `use crate::X` which only works if consuming crate re-exports generated modules at root. Document in README/DECISIONS.md. Not a code change.
60
60
61
+
#### Architecture (Medium Priority)
62
+
-[ ]**Defense-in-depth: validate local_tests[].expect at the sink** — `generate_code` (generator.rs:19) is a public library function that embeds `local_test.expect` verbatim with no validation. The CLI path always validates first via the loader, but a direct library API caller constructing a `ResolvedSpec` manually bypasses all expression validation. Consider: (a) validate at the `generate_code` sink, (b) use a newtype wrapper for validated expect strings, or (c) emit the generated assert!() from the validated syn::Expr AST instead of the raw string. Codex outside-voice finding, fix/change-is_safe_expect_expr review.
63
+
-[ ]**Add recursion depth cap to `is_safe_expect_expr`** — The recursive AST walk has no depth limit. Deeply nested input like `((((x))))` or `!!!!!x` (100+ levels) could stack overflow during validation. Add a `depth: usize` parameter and return `false` above a threshold (~128). Low-urgency since `.unit.spec` files are trusted input, but the fix is trivial. Codex adversarial finding, fix/change-is_safe_expect_expr review.
64
+
61
65
#### M3 Prerequisites (Design Spikes, Before Build)
62
66
-[ ]**Define ICP: solo engineer vs. team coordination tool** — Changes M3 priority order completely. One paragraph, before M3 scoping.
63
67
-[ ]**Force binary decision: commit generated output vs ephemeral** — Current hybrid (gitignored but required to compile) is unstable for real adopters. Decide and document in DECISIONS.md.
// closures, and control-flow forms (If, Match, Loop, etc.) that introduce
135
+
// statements or scope inside assert!(). Note: call expressions to
136
+
// side-effectful functions (std::fs, std::process::Command, etc.) are not
137
+
// blocked — .unit.spec files are treated as trusted input, the same as
138
+
// body.rust. A config lever and defense-in-depth options are deferred to
139
+
// M3. See TODOS.md.
137
140
if !is_safe_expect_expr(&expr){
138
141
returnErr(SpecError::LocalTestExpectNotExpr{
139
142
id: test.id.clone(),
140
-
message:"expect must be a simple expression (binary, call, path, or literal); block, unsafe, closure, and control-flow expressions are not allowed".to_string(),
143
+
message:"expect must use only operators, function calls, and value access; block, unsafe, closure, and control-flow forms are not allowed".to_string(),
0 commit comments