Implement sed builtin command#56
Conversation
…ening Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a6ffbb71e1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
I can only run on private repositories. |
AlexandreYang
left a comment
There was a problem hiding this comment.
Code Review: Implement sed builtin command
Overall this is an excellent, well-structured implementation of sed as a safe builtin. The security hardening (blocked commands, RE2 regex, memory limits, branch iteration caps) is thorough and well-tested. The architecture split across types.go, parser.go, engine.go, and sed.go is clean.
Below are findings organized by severity.
P1 — Correctness / Bash Compatibility
1. s/// substitution: subMade flag not set when replacement equals original match
In execSubstitute (engine.go), the condition if result != eng.patternSpace means that if a substitution matches but the replacement produces the same string (e.g., s/x/x/), subMade is never set to true. In GNU sed, t considers any successful match as "substitution made", regardless of whether the result differs. This will cause t/T branches to behave incorrectly for identity substitutions.
2. readSubstPart silently accepts missing closing delimiter
In parser.go, readSubstPart returns the accumulated string without error when the input is exhausted before finding the closing delimiter. This means s/foo/bar (missing final /) is silently accepted. While GNU sed also accepts a missing final delimiter for the replacement part, the pattern part should require its closing delimiter. The current code calls readSubstPart for both pattern and replacement, so s/foo (missing replacement delimiter too) is silently accepted with an empty replacement. This is inconsistent with the strict error handling elsewhere in the parser.
3. c (change) command with address ranges
In the engine, cmdChange always prints the text and returns actionDelete on every matching line. In GNU sed, when used with a range address (e.g., 2,4c\text), the c command should output text only once (when the range ends), not for every line in the range. The current implementation prints it for every line in the range.
P2 — Potential Issues
4. $ address matching across multiple files
The lastLine detection via lineReader.isLast() only looks at the current file's reader, so $ matches the last line of each file. In GNU sed (without -s), $ only matches the last line of the last file. Since -s is not supported, this is a compatibility difference for multi-file $ usage.
5. N command at end of input loses append queue
When cmdNextAppend cannot read another line, it prints the pattern space explicitly and returns actionDelete. This skips the runCycle append queue flush, so any text queued by prior a commands would be lost.
6. expandReplacement handles \n and \t — but these were already decoded by readSubstPart
In parser.go, readSubstPart converts \n to a literal newline and \t to a literal tab at parse time. Then expandReplacement also has \n/\t handling. Since readSubstPart already consumed the backslash sequences, the \n/\t cases in expandReplacement are dead code for the substitution replacement path. This is not a bug today but is misleading.
P3 — Minor / Style
7. blocked_inplace.yaml checks for pflag error, not a sed-specific message
The test expects stderr_contains: ["unknown shorthand flag"] which is the generic pflag rejection, not a deliberate sed message. Other blocked features (e/w/r/R/W) have clear custom messages. Consider registering -i as a recognized flag that returns a specific error.
8. no_script.yaml scenario will likely fail bash comparison
The scenario has stderr: "sed: no script command has been specified\n" — GNU sed's error message differs. This should either be marked skip_assert_against_bash: true or use stderr_contains.
9. Dead code in TestPentestManyFiles
The filename generation (lines 217-220) computes name then immediately shadows it with actualName. The initial name computation is unused.
10. breToERE doc comment about backreferences is misleading
The comment says backreferences are "passed through unchanged" — but RE2 does not support them, so they will cause a compile error or silent failure. Consider clarifying.
11. Scenario test missing_file.yaml creates a dummy.txt that is never used
The setup creates dummy.txt but the test only references nonexistent.txt. The dummy file can be removed.
Security Assessment
The security posture is strong:
- All dangerous sed commands (
e,w,W,r,R) and flags (-i,-f) blocked at parse time - RE2 prevents ReDoS
- Pattern/hold space limits prevent memory exhaustion
- Branch iteration cap prevents infinite loops
- Total read limit guards against infinite stdin sources
- Context cancellation checked in main loop
- Pentest suite is comprehensive
Summary
| Severity | Count | Key Items |
|---|---|---|
| P1 | 3 | subMade identity substitution, missing delimiter parsing, c command range behavior |
| P2 | 3 | $ multi-file, N at EOF loses appends, dead replacement escaping |
| P3 | 5 | Minor test/style issues |
Good work overall. The P1 items around subMade and c command range behavior should be addressed before merge as they affect bash compatibility.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3500bd80a9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…egation, and blocked commands Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
I can only run on private repositories. |
AlexandreYang
left a comment
There was a problem hiding this comment.
Code Review: sed builtin (PR #56)
Overview
Reviewed the full sed builtin implementation: parser, engine, types, registration, tests (unit + hardening + pentest), and 80+ YAML scenario files. The implementation is well-structured with strong security foundations (RE2 regex, sandbox-aware file access via callCtx.OpenFile, blocked dangerous commands/flags, resource limits on pattern/hold space, branch iterations, and read bytes).
Assessment: needs fixes (2 P1, 3 P2, 2 P3)
Positive Observations
- Security: All file access goes through
callCtx.OpenFile— no directos.Open/os.ReadFile. Dangerous commands (e,w,W,r,R) and flags (-i,-f) are blocked at parse time. Theeandwsubstitution flags are also blocked. - DoS protection: Pattern space, hold space, line length, branch iterations, and total read bytes are all bounded. RE2 guarantees linear-time matching.
- Import discipline: Only safe
ossymbols (O_RDONLY,FileInfo) are used. Noos/exec,net,unsafe, orreflect. - Context cancellation: Checked in both the main loop and command execution loop.
- Test coverage: Comprehensive Go unit tests, hardening tests, pentest tests, and 80+ YAML scenario files covering addresses, branching, hold space, transliteration, edge cases, and blocked commands. Blocked-command scenarios correctly set
skip_assert_against_bash: true. - Code organization: Clean split into types, parser, engine, and entrypoint files.
|
Iteration 1 Self-Review Result: REQUEST_CHANGES
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 335f784d11
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Fix subMade to use match detection instead of string equality (P1) s/a/a/ is now considered a successful substitution for t/T/p - Escape literal $ in replacement strings (P1) expandReplacement now writes $$ so Go regexp doesn't interpret $1 etc. - Fix c command with range to output text only once (P2) Range addresses now silently delete until end of range - Fix readSubstPart to reject unterminated substitutions (P2) s/foo/bar without closing delimiter now errors like GNU sed - Fix n command at EOF to not duplicate last line (P1) Return actionDelete to suppress auto-print after n already printed - Fix T command to reset subMade after evaluation (P1) Both t and T now clear the substitution flag - Fix branch to label inside group (P2) findLabel now returns a two-level path for in-group labels - Remove dead \n/\t code in expandReplacement (P2) These escapes are already handled at parse time - Mark pre-existing bash divergences with skip_assert_against_bash - Add 8 new test scenarios covering all fixed behaviors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This reverts commit ed64b73.
This reverts commit 47447f8.
|
I can only run on private repositories. |
|
I can only run on private repositories. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 31cf625a8f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Iteration 3 self-review result: APPROVE — 0 findings. No code changes since iteration 2. Waiting for @codex to respond to review request. |
|
I can only run on private repositories. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 31cf625a8f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Review-Fix Loop Summary
Iteration log
Final state
Remaining issues |
|
No follow-up code changes were needed. The trigger indicates the PR is already clean (all findings resolved, all threads resolved, CI passing), and the local branch is also clean with no uncommitted work. Checks run
|
|
I can only run on private repositories. |
AlexandreYang
left a comment
There was a problem hiding this comment.
Code Review: sed builtin implementation (PR #56)
Overview
This PR adds a full sed stream editor builtin to the restricted shell. The implementation includes:
- A sed script parser (
parser.go) with BRE-to-ERE conversion - An execution engine (
engine.go) with all standard sed commands - Proper blocking of dangerous commands (
e,w,W,r,R) and flags (-i,-f) - Comprehensive resource limits (pattern/hold space, branch iterations, line size, total read)
- Extensive test coverage: ~60 YAML scenario tests, Go functional tests, hardening tests, and pentest tests
Overall Assessment: Safe to merge with minor suggestions
This is an impressively thorough implementation. The security posture is strong:
- All file access goes through
callCtx.OpenFile(sandbox-safe) - Dangerous sed commands/flags are blocked at parse time
- RE2 guarantees linear-time regex matching (no ReDoS)
- Resource limits prevent memory exhaustion and infinite loops
- Context cancellation is properly threaded through the execution loop
- Import allowlist additions are minimal and justified (
bytes.IndexByte,bufio.Scanner)
Summary of Findings
Test Coverage Summary
| Code path | Scenario test | Go test | Status |
|---|---|---|---|
| Basic substitution (s///) | tests/scenarios/cmd/sed/substitute/basic.yaml + 15 more | sed_test.go | Covered |
| Global/Nth/print flags | substitute/global.yaml, nth.yaml, print_flag.yaml | sed_test.go | Covered |
| Case-insensitive flag | substitute/case_insensitive.yaml | sed_test.go | Covered |
| Address types (line, $, regex, step, range) | address/*.yaml (12 scenarios) | sed_test.go | Covered |
| Negation (!) | address/negation*.yaml (3 scenarios) | sed_test.go | Covered |
| Branch/label (b/t/T) | branch/*.yaml (5 scenarios) | sed_test.go | Covered |
| Hold space (h/H/g/G/x) | hold/*.yaml (3 scenarios) | sed_test.go | Covered |
| Text commands (a/i/c) | text/*.yaml (5 scenarios) | sed_test.go | Covered |
| Next line (n/N) | next/*.yaml (8 scenarios) | sed_test.go | Covered |
| Print (p/P/=/l) | print/*.yaml (5 scenarios) | sed_test.go | Covered |
| Quit (q/Q) | quit/*.yaml (4 scenarios) | sed_test.go | Covered |
| Delete (d/D) | delete/*.yaml (4 scenarios) | sed_test.go | Covered |
| Group commands ({}) | group/*.yaml (5 scenarios) | sed_test.go | Covered |
| Transliterate (y) | transliterate/basic.yaml | sed_test.go | Covered |
| Blocked commands (e/w/W/r/R) | errors/blocked_*.yaml (7 scenarios) | sed_hardening_test.go, sed_pentest_test.go | Covered |
| Resource limits | — | sed_hardening_test.go, sed_pentest_test.go | Covered |
| Error cases | errors/*.yaml (12 scenarios) | sed_test.go | Covered |
| Edge cases (CRLF, empty file, no newline) | edge/*.yaml (4 scenarios) | sed_test.go | Covered |
| Stdin/pipe | stdin/*.yaml (2 scenarios) | sed_test.go | Covered |
| Multiple files | — | sed_hardening_test.go | Covered |
| Context cancellation | — | sed_hardening_test.go, sed_pentest_test.go | Covered |
| ReDoS resilience | — | sed_pentest_test.go | Covered |
| Path/symlink edge cases | — | sed_pentest_test.go | Covered |
Overall coverage: Adequate — all new code paths are covered by scenario and/or Go tests.
Positive Observations
- Sandbox integrity: All file I/O goes through
callCtx.OpenFile— no directos.Open/os.ReadFile/os.Statcalls. - Parse-time validation: Labels are validated at parse time, backreferences are checked before execution, and blocked commands/flags are rejected immediately.
- Resource limits are comprehensive: MaxLineBytes, MaxSpaceBytes, MaxBranchIterations, MaxTotalReadBytes, and MaxAppendQueueBytes cover all growth vectors.
- RE2 regex engine: Eliminates ReDoS entirely — critical for a shell that runs untrusted scripts.
- Excellent test coverage: ~60 YAML scenario tests covering addresses, branching, substitution, hold space, quit, and edge cases. Pentest tests cover resource exhaustion, blocked commands, path edge cases, and flag injection. Hardening tests verify memory limits.
- Well-structured code: Clean separation between parser, engine, and types. The label map precomputation for O(1) branch lookup is a nice touch.
- Documentation updated:
SHELL_FEATURES.mdcorrectly documents the new builtin with blocked features.
|
Iteration 1 self-review result: COMMENT
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 75618d8118
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if ctx.Err() != nil { | ||
| break | ||
| } | ||
| isLastFile := i == len(files)-1 | ||
| if err := eng.processFile(ctx, callCtx, file, isLastFile); err != nil { |
There was a problem hiding this comment.
Preserve continuous stream semantics across input files
GNU sed documents -s, --separate as the option that treats files separately, which means the default behavior is one continuous input stream across all files; however this loop processes each file with a separate processFile pass, so commands that must read the “next line” (n/N) hit end-of-file at file boundaries instead of continuing into the next file. In practice this diverges from GNU sed (e.g., sed -n '1{n;p}' f1 f2 should print the first line of f2, but here it prints nothing), so multi-file scripts using n/N can produce incorrect results.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
[Claude Opus 4.6] This is the same known architectural limitation raised in multiple other threads. The per-file processFile design does not support unified stream semantics across files. This would require a fundamental redesign to implement a stream-level reader that spans file boundaries. Tracked as future work.
…rser - Enforce command separators after zero-arg commands (p, d, n, h, etc.) so 'pp', 'dp' etc. are rejected with "extra characters after command" matching GNU sed behavior. - Accept '#' as inline comment after q/Q commands (with or without exit code), matching GNU sed behavior. - Reject repeated numeric occurrence flags in s/// with "multiple number options to 's' command", matching GNU sed. - Reject case-insensitive flag (i/I) on empty regexp reuse with "cannot specify modifiers on empty regexp", matching GNU sed. - Resolve duplicate labels to last definition (GNU sed branches to the most recently defined label, not the first). - Remove dead subCaseInsensitive field and runtime code. - Add clarifying comment in readSubstPart for non-special escape handling. - Add scenario description explaining intentional trailing-newline divergence. - Add scenario tests for all new behaviors and N at EOF. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Review-Fix Loop Summary
Iteration log
Final state
Remaining issues
Fixes applied in this iteration
🤖 Generated with Claude Code |
|
No follow-up code changes were needed. Given the trigger’s final state (CLEAN, no unresolved comments, CI passing) and the local branch state, there was nothing to modify, so I did not create a commit and did not call Checks
|
|
/merge |
|
View all feedbacks in Devflow UI.
The expected merge time in
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bed3120466
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| sb.WriteByte('\\') | ||
| sb.WriteByte(next) |
There was a problem hiding this comment.
Drop nonspecial escapes in address regex parsing
In readUntilDelimiter, escaped characters other than the delimiter are always preserved as \X, which makes nonspecial escapes like \q reach RE2 unchanged and fail compilation. GNU sed treats nonspecial escapes in address regexes as literals (checked locally with GNU sed 4.9: printf 'q\n' | sed -n '/\\q/p' prints q), so this diverges by rejecting valid scripts with invalid escape sequence errors.
Useful? React with 👍 / 👎.
| // character must be a command separator, '#' (inline comment), or EOF. | ||
| if p.pos < len(p.input) { | ||
| ch := p.input[p.pos] | ||
| if ch != ';' && ch != '\n' && ch != '\r' && ch != '}' && ch != ' ' && ch != '\t' && ch != '#' { |
There was a problem hiding this comment.
Reject extra tokens after q/Q numeric exit code
The post-digit separator check accepts a space/tab as a complete terminator, so q1 p is parsed as q 1 plus another command instead of failing. GNU sed rejects this form (extra characters after command; verified locally with GNU sed 4.9 via printf 'a\n' | sed 'q1 p'), so this currently accepts malformed scripts that should produce a parse error.
Useful? React with 👍 / 👎.
| for p.pos < len(p.input) && p.input[p.pos] != ' ' && p.input[p.pos] != '\t' && | ||
| p.input[p.pos] != '\n' && p.input[p.pos] != ';' && p.input[p.pos] != '}' { |
There was a problem hiding this comment.
Treat '#' as terminator for branch label arguments
parseLabelArg does not stop at #, so branch forms like b #comment and t #comment are parsed as branching to label #comment and fail validation. GNU sed treats # as an inline comment at that point (checked locally with GNU sed 4.9: printf 'x\n' | sed 'b #c; s/x/y/' succeeds), so valid commented branch scripts regress to hard errors here.
Useful? React with 👍 / 👎.
Summary
sedstream editor as a safe builtin with full script parser, address matching, and command execution engines,p,d,P,D,q,Q,y,a,i,c,=,l,n,N,h,H,g,G,x,b,t,T,{...},!) and flags (-n,-e,-E/-r)e,w,W,r,R) and flags (-i,-f) to prevent command execution and file system accessTest plan
go test ./interp/... ./tests/...— all tests passe,w,W,r,R,s///w,s///e) return exit 1 with "blocked" error-iflag rejectedRSHELL_BASH_TEST=1 go test ./tests/ -run TestShellScenariosAgainstBash -timeout 120s— bash comparison🤖 Generated with Claude Code