Skip to content

feat(rm): implement rm builtin for remediation mode#556

Draft
julesmcrt wants to merge 10 commits into
mainfrom
jules.macret/rm-remediation-mode
Draft

feat(rm): implement rm builtin for remediation mode#556
julesmcrt wants to merge 10 commits into
mainfrom
jules.macret/rm-remediation-mode

Conversation

@julesmcrt

Copy link
Copy Markdown
Collaborator

Summary

  • Adds a minimal, hardened rm builtin for remediation mode: non-recursive file deletion only, with -v/--verbose and -h/--help as the only accepted flags (-r/-R/--recursive, -f, -i/-I, -d, --preserve-root, --no-preserve-root, --one-file-system are all rejected as unknown flags).
  • Directories are never removed (even empty ones, kept consistent across macOS/Linux since unlinkat silently succeeds on empty dirs on macOS); symlinks are unlinked without following their target; each invocation is capped at 10 file operands, checked before any file is removed.
  • New Sandbox.Remove / CallContext.Remove capabilities, wired the same way as the existing truncate/logrotate remediation-mode builtins. Includes a resolveRemoveTarget fix so removal correctly follows unlink(2) semantics (never resolving the final path component) — caught via pentest fuzzing on self-referential and sandbox-escaping symlinks.
  • Scenario, unit, pentest, and fuzz test coverage (FuzzRmFilename, FuzzRmOperandCount, wired into .github/workflows/fuzz.yml); README.md and SHELL_FEATURES.md updated to document rm alongside truncate/logrotate.

Test plan

  • go build ./..., go vet ./..., gofmt -l . all clean
  • go test ./... passes across the full repo
  • go test ./builtins/tests/rm/... -v (unit + pentest, 34 tests) passes
  • go test ./allowedpaths/... -run TestSandboxRemove -v passes
  • Short local fuzzing (-fuzztime=20s) on both new fuzz targets found no crashes
  • Verified help / help rm output in both read-only and remediation modes
  • RSHELL_BASH_TEST=1 go test ./tests/ -run TestShellScenariosAgainstBash -timeout 120snot run locally (no Docker in this environment); please run in CI or locally before merging

🤖 Generated with Claude Code

Adds a minimal, hardened `rm` builtin scoped to non-recursive file
deletion: only -v/--verbose and -h/--help are supported, directories
are never removed (even empty ones, consistently across platforms),
symlinks are unlinked without following, and each invocation is capped
at 10 file operands to bound the blast radius of a mistaken glob.

Introduces Sandbox.Remove and CallContext.Remove, wired the same way as
the existing truncate/logrotate remediation-mode capabilities. Adds
scenario, unit, pentest, and fuzz coverage, and documents rm alongside
truncate/logrotate in README.md and SHELL_FEATURES.md.
…l commit

A failed pathspec in the prior `git add` silently aborted staging for
sandbox.go, sandbox_test.go, rm.go, and the docs before the first
commit landed, so it shipped the pre-fix Sandbox.Remove (which reused
resolveWriteTarget and incorrectly followed the final path component,
breaking removal of self-referential/escaping symlinks) instead of the
resolveRemoveTarget fix, plus the errors.New idiom cleanup and the
README/SHELL_FEATURES/fuzz.yml doc updates.
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 14c08bca86

ℹ️ 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".

Comment thread allowedpaths/sandbox.go Outdated
Comment thread SHELL_FEATURES.md Outdated
julesmcrt and others added 2 commits July 17, 2026 11:59
Sandbox.Remove previously ran a symlink-component precheck and an Lstat
directory check, then called os.Root.Remove separately on the same path —
leaving a race window where an intermediate component swapped for a symlink
between the checks and the removal could redirect the delete into a
different, more-restrictive AllowedPaths root. Add writeopen.Unlink, an
atomic no-follow openat-walk-then-unlinkat on Unix (mirroring the existing
write-target walker) and a Lstat-then-Remove call on Windows (which already
gets atomicity from os.Root's O_NOFOLLOW_ANY), and wire Sandbox.Remove
through it via a new (*root).removeFile method.

Also fix rm's documentation (SHELL_FEATURES.md, README.md,
CallContext.Remove's doc comment), which incorrectly claimed only regular
files and symlinks are removable — FIFOs, sockets, and device nodes are
also removable, since the implementation only excludes directories.

Addresses automated review feedback on PR #556.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e crashes

The byte-level filter (reject NUL/single-quote in the raw name) wasn't
sufficient: some names combine with the appended closing quote in ways the
shell lexer's line-continuation handling still rejects as unparseable (CI
found "1:4: reached EOF without closing quote" on corpus entry
be32d37903cefe74). Parse the actual constructed script with the real parser
and skip anything it rejects, mirroring how the du fuzz test already
handles this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92ff7efcbf

ℹ️ 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".

Comment thread builtins/rm/rm.go
Comment thread allowedpaths/internal/writeopen/writeopen_windows.go Outdated
The prior implementation did a separate Lstat then Remove; since Windows'
os.Root.Remove can delete empty directories, a file-to-directory swap in
that window could make rm delete a directory. Replace it with a single
NtCreateFile (FILE_NON_DIRECTORY_FILE + FILE_OPEN_REPARSE_POINT) followed
by NtSetInformationFile (FILE_DISPOSITION_DELETE) on the same handle, so
the type check and the delete apply to the exact same open object.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c74163e41e

ℹ️ 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".

Comment thread allowedpaths/internal/writeopen/writeopen_unix.go
julesmcrt and others added 2 commits July 17, 2026 17:33
…ing them

rm file/, rm symlink-to-dir/, and rm file/. all require their operand to
resolve as a directory per POSIX/GNU semantics. filepath.Clean drops the
trailing separator before the sandbox unlink path is reached, so rm was
deleting the wrong target instead of failing with Not a directory / Is a
directory like GNU/BSD rm. Detect the syntactic requirement in rm.go itself
(before any Clean-based path handling) and re-check with a following Stat
to disambiguate the two error cases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TestRmSymlinkToDirectoryTreatedAsSymlink was failing on windows-latest CI:
NtCreateFile with FILE_NON_DIRECTORY_FILE rejects opening a symlink/junction
whose target is a directory with STATUS_FILE_IS_A_DIRECTORY, because NTFS
sets FILE_ATTRIBUTE_DIRECTORY on the reparse point itself, not just its
target. That's a real directory check bypassing FILE_OPEN_REPARSE_POINT's
"operate on the link, not what it points to" semantics.

Drop FILE_NON_DIRECTORY_FILE from the create flags and instead check
FileAttributes after open: a directory attribute without a reparse-point
attribute is a genuine directory (rejected); a reparse point is always
removable regardless of what it resolves to, matching Lstat-based
directory detection on Unix.

Also guard rm.go's pre-existing IsDir() directory check against symlinks,
for the same underlying reason on the LstatFile path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e60e35d489

ℹ️ 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".

Comment thread allowedpaths/internal/writeopen/writeopen_unix.go
Comment thread builtins/rm/rm.go Outdated
…ix backslash handling

Unlink now rejects a syntactically-directory operand (trailing "/", or a
final "." / "..") whose target isn't actually a directory, closing the gap
where only rm.go's own precheck enforced this. Sandbox.Remove captures that
intent on the raw path before toAbs's filepath.Join strips the trailing
separator, and re-encodes it onto relPath so Unlink's check actually fires.

Also fixes rm.go's hasTrailingDirSyntax treating "\" as a separator on all
platforms — on Unix it's a valid filename character, so `rm 'foo\'` was
incorrectly rejected as "Not a directory" instead of removing the file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b63dc790cc

ℹ️ 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".

Comment thread builtins/rm/rm.go Outdated
Comment thread allowedpaths/sandbox.go
…ore dereferencing on rm

rm --verbose=false/--help=false previously parsed via pflag's BoolP and
proceeded to delete files, diverging from GNU rm's no_argument longopt
behavior. Reuse df's noArgBool pattern (now exported from flagparser) so
an explicit value on --help/--verbose is rejected before any file is
touched.

Sandbox.Remove's trailing-dir-syntax handling forces Unlink to dereference
a same-root symlink's final component via a raw Fstatat/NtCreateFile call
that never revalidated the target against AllowedPaths. Resolve the full
symlink chain and require it to land inside a configured root before
Unlink is allowed to dereference it, closing a sandbox-boundary stat leak
for `rm link/` where link points outside every AllowedPaths root. Also
fixes rm.go's own trailing-slash precheck, which previously masked the
real StatFile error (e.g. permission denied, dangling symlink) as a
blanket "Not a directory".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 011c8156d2

ℹ️ 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".

Comment thread builtins/rm/rm.go Outdated
removeFile only re-checked trailing-slash operands (file/, dangling
symlink/, self-referential symlink/) via StatFile when the initial
LstatFile call succeeded. LstatFile dereferences the trailing slash
itself and fails with ENOENT/ELOOP for dangling and self-referential
symlinks, so those cases fell through to a raw callCtx.Remove call
that leaked Sandbox.Stat's internal error formatting instead of a
clean GNU-style message.

Decouple the hasTrailingDirSyntax check from LstatFile's error so the
StatFile-based switch always runs for trailing-slash operands. Also
fix Sandbox.Stat/Lstat to rebuild their error using the caller's
op/path instead of os.Root's internal "statat"/sandbox-relative path,
so the normalized error identity (fs.ErrNotExist, etc.) survives a
second PortableErrMsg pass at the builtin layer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@julesmcrt

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1803ac4b6f

ℹ️ 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".

Comment thread allowedpaths/sandbox.go
// trailing-dir-syntax enforcement still sees it.
requiresDir := writeopen.HasTrailingDirSyntax(path)

absPath := toAbs(path, cwd)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve symlink components before resolving rm targets

When the operand contains a symlinked intermediate component followed by .., this toAbs call uses filepath.Join and lexically collapses the path before the sandbox has a chance to apply filesystem path-resolution semantics. For example, with link -> /outside/sub, rm link/../file should resolve through the symlink and be denied or target /outside/file (GNU rm does the latter), but here it becomes <cwd>/file and deletes that in-sandbox file instead. That makes remediation-mode rm remove a different file than the operand names whenever symlink/.. appears in the path.

Useful? React with 👍 / 👎.

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