Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
ffd5112
Implement find builtin command
matt-dz Mar 10, 2026
fdfcda6
Address code review findings for find builtin
matt-dz Mar 10, 2026
45331b5
Fix misplaced skip_assert_against_bash in nonexistent.yaml
matt-dz Mar 10, 2026
b3d04ee
Document ReadDir sorted-order design choice and its bash divergence
matt-dz Mar 10, 2026
6188545
Address remaining PR review comments
matt-dz Mar 11, 2026
10e4148
Use fmt.Errorf directly instead of errors.New(fmt.Sprintf())
matt-dz Mar 11, 2026
682a62b
Add 52 comprehensive test scenarios for find builtin
matt-dz Mar 11, 2026
2691ffb
Fix -newer cache bug and address PR review comments
matt-dz Mar 11, 2026
7335e90
Integrate -maxdepth/-mindepth into the expression parser
matt-dz Mar 11, 2026
27b3d5e
Detect symlink loops by file identity (dev+inode) instead of path str…
matt-dz Mar 11, 2026
139d228
Address PR #36 review comments (round 2)
matt-dz Mar 11, 2026
27c52f1
Merge branch 'main' of github.com:DataDog/rshell into matt-dz/impleme…
matt-dz Mar 11, 2026
1698078
Address PR #36 review comments (round 3)
matt-dz Mar 11, 2026
e823302
Address PR #36 review comments (round 4)
matt-dz Mar 11, 2026
c95fb6c
Address PR #36 review comments (round 5)
matt-dz Mar 11, 2026
cd0786f
Address PR #36 review comments (round 6)
matt-dz Mar 11, 2026
4fc005f
Address PR #36 review comments (round 7)
matt-dz Mar 11, 2026
5d1151a
Address PR #36 review comments (round 8)
matt-dz Mar 11, 2026
4afc9fb
Address PR #36 review comments (round 9)
matt-dz Mar 11, 2026
b187f82
Address PR #36 review comments (round 10)
matt-dz Mar 11, 2026
f87d171
Address PR #36 review comments (round 11)
matt-dz Mar 11, 2026
793f6da
Address PR #36 review comments (round 12)
matt-dz Mar 11, 2026
a081c95
Address PR #36 review comments (round 13)
matt-dz Mar 11, 2026
92d809d
Add comprehensive unit tests for find builtin regression prevention
matt-dz Mar 11, 2026
289e635
Address PR #36 review comments (round 14)
matt-dz Mar 12, 2026
c75c0d9
Address PR #36 review comments (round 14)
matt-dz Mar 12, 2026
935ffd1
Address PR #36 review comments (round 15)
matt-dz Mar 12, 2026
ccea45b
Address PR #36 review comments (round 16)
matt-dz Mar 12, 2026
89730d0
Add regression tests for round 16 fixes
matt-dz Mar 12, 2026
ee3c48a
Merge branch 'main' into matt-dz/implement-find-builtin
matt-dz Mar 12, 2026
00825cc
Fix loop_detection_with_L test panic on Windows
matt-dz Mar 12, 2026
96351e6
Address PR #36 review comments (round 18)
matt-dz Mar 12, 2026
b6e4e11
Address PR #36 review comments (round 19)
matt-dz Mar 12, 2026
7386574
Fix gofmt and Windows ls sandbox test
matt-dz Mar 12, 2026
e9e8085
Address Effective Go review findings on find builtin
matt-dz Mar 12, 2026
fcbda04
Add time.Duration and time.Minute to builtin allowed symbols
matt-dz Mar 12, 2026
2e3d805
Address Effective Go review findings (round 2)
matt-dz Mar 12, 2026
a939740
Fix TestAllowedPathsExecViaPathLookup bypassing sandbox
matt-dz Mar 12, 2026
43b8b56
Merge branch 'main' of github.com:DataDog/rshell into matt-dz/impleme…
matt-dz Mar 12, 2026
c1a2380
Revert "Fix TestAllowedPathsExecViaPathLookup bypassing sandbox"
matt-dz Mar 12, 2026
3d540c5
Fix TestAllowedPathsExecViaPathLookup: sed is now a builtin
matt-dz Mar 12, 2026
d6da039
Address PR review comments
matt-dz Mar 12, 2026
e2e511e
Address PR review comments (round 3)
matt-dz Mar 13, 2026
b29a889
Add -mmin overflow tests and fix parser for int64-exceeding values
matt-dz Mar 13, 2026
3665f7c
Add scenario tests for ')' treated as path operand
matt-dz Mar 13, 2026
cdb1ad8
Add isExpressionStart unit test and path/expression boundary scenarios
matt-dz Mar 13, 2026
b961c4f
Add missing test coverage for find builtin
matt-dz Mar 13, 2026
00e872e
Merge branch 'main' into matt-dz/implement-find-builtin
matt-dz Mar 13, 2026
fbe0aaf
format files
matt-dz Mar 13, 2026
1ecb071
Merge branch 'matt-dz/implement-find-builtin' of github.com:DataDog/r…
matt-dz Mar 13, 2026
894fcd4
Fix bash comparison failures due to find output ordering
matt-dz Mar 13, 2026
6bc9c94
Use unsorted ReadDir in find to match GNU find ordering
matt-dz Mar 13, 2026
e21613f
Merge branch 'main' into matt-dz/implement-find-builtin
matt-dz Mar 13, 2026
1fd9265
Fix CI failures from unsorted find output on Linux
matt-dz Mar 13, 2026
5859439
Streaming DFS walker and short-circuit evalEmpty for find
matt-dz Mar 13, 2026
9203b69
Merge branch 'main' into matt-dz/implement-find-builtin
matt-dz Mar 13, 2026
49d858a
Fix CI: add os.File to builtin allowlist, remove stale RuneCount entry
matt-dz Mar 13, 2026
11bf6c0
fix: address find review comments (empty paths, empty -newer, malform…
matt-dz Mar 13, 2026
7f957a1
fix: treat empty path operands as per-root errors, not fatal parse
matt-dz Mar 13, 2026
af78619
test: add scenario tests for -mtime -0/0/+0 edge cases
matt-dz Mar 13, 2026
ff78c00
fix: make -type f and -type d scenarios order-independent
matt-dz Mar 13, 2026
f96ebbf
fix: stabilize -newer scenario with explicit mod_time values
matt-dz Mar 13, 2026
ba66935
revert: drop unrelated ls sandbox test change from find PR
matt-dz Mar 13, 2026
7375410
fix: align -mtime +N/-N with GNU find's raw-second comparison
matt-dz Mar 13, 2026
257315f
fix CI: add time.Hour and time.Second to builtin allowlist
matt-dz Mar 13, 2026
90dd433
refactor: replace *os.File with fs.ReadDirFile in OpenDir
matt-dz Mar 13, 2026
a9fbd9a
fix: propagate context cancellation + fix future-dated file mtime mat…
matt-dz Mar 13, 2026
2396dab
fix: stabilize Windows CI tests
matt-dz Mar 13, 2026
f262c4d
Merge remote-tracking branch 'origin/main' into matt-dz/implement-fin…
matt-dz Mar 13, 2026
c3b4bfa
fix: correct wc stdin/no_filename test expectation to match bash
matt-dz Mar 13, 2026
cced11b
fix: capture invocation time once for consistent -mtime/-mmin evaluation
matt-dz Mar 13, 2026
4a43391
test: verify Now() is called once per find invocation
matt-dz Mar 13, 2026
f4b613a
fix: fall back to lstat for dangling -newer refs under -L
matt-dz Mar 13, 2026
2cd4a86
fix: stabilize newer_dangling_symlink_L with explicit mod_time
matt-dz Mar 13, 2026
9f64956
Merge branch 'main' of github.com:DataDog/rshell into matt-dz/impleme…
matt-dz Mar 13, 2026
f45ea57
fix: use stderr_contains_windows for ls sandbox test
matt-dz Mar 13, 2026
32c6ee0
fix: stabilize mmin_exact test with explicit mod_time
matt-dz Mar 13, 2026
e4e0849
Merge remote-tracking branch 'origin/main' into matt-dz/implement-fin…
matt-dz Mar 13, 2026
1533016
Merge branch 'main' into matt-dz/implement-find-builtin
AlexandreYang Mar 16, 2026
c833dc2
Merge branch 'main' into matt-dz/implement-find-builtin
matt-dz Mar 16, 2026
0d61bb9
fix: restrict -L lstat fallback to "not found" errors only
matt-dz Mar 16, 2026
36324e4
fix: always use lstat for -newer reference files, matching GNU find
matt-dz Mar 16, 2026
a82d068
test: enable bash comparison for all -newer scenarios
matt-dz Mar 16, 2026
3485a2b
fix: normalize Windows path separators in find builtin
matt-dz Mar 16, 2026
0951ce6
test: fix misleading parsePathPredicate test name and comments
matt-dz Mar 16, 2026
5862a0a
test: add Windows-specific tests for path separator normalization
matt-dz Mar 16, 2026
92aa8c4
format
matt-dz Mar 16, 2026
5600a9f
fix: follow symlink targets for -newer reference when -L is set
matt-dz Mar 16, 2026
16dc823
test: add -L predicate interaction tests verified against GNU find
matt-dz Mar 16, 2026
37e50e7
test: add -L -mtime/-mmin symlink tests, move Windows tests inline
matt-dz Mar 16, 2026
954c88b
fix: treat trailing backslash glob as non-matching per GNU fnmatch
matt-dz Mar 16, 2026
c2576b0
fix: remove backslash-named file from scenario (illegal on Windows)
matt-dz Mar 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions SHELL_FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Blocked features are rejected before execution with exit code 2.
- ✅ `echo [-neE] [ARG]...` — write arguments to stdout; `-n` suppresses trailing newline, `-e` enables backslash escapes, `-E` disables them (default)
- ✅ `exit [N]` — exit the shell with status N (default 0)
- ✅ `false` — return exit code 1
- ✅ `find [-L] [PATH...] [EXPRESSION]` — search for files in a directory hierarchy; supports `-name`, `-iname`, `-path`, `-ipath`, `-type`, `-size`, `-empty`, `-newer`, `-mtime`, `-mmin`, `-maxdepth`, `-mindepth`, `-print`, `-print0`, `-prune`, logical operators (`!`, `-a`, `-o`, `()`); blocks `-exec`, `-delete`, `-regex` for sandbox safety
- ✅ `grep [-EFGivclLnHhoqsxw] [-e PATTERN] [-m NUM] [-A NUM] [-B NUM] [-C NUM] PATTERN [FILE]...` — print lines that match patterns; uses RE2 regex engine (linear-time, no backtracking)
- ✅ `head [-n N|-c N] [-q|-v] [FILE]...` — output the first part of files (default: first 10 lines); `-z`/`--zero-terminated` and `--follow` are rejected
- ✅ `sort [-rnubfds] [-k KEYDEF] [-t SEP] [-c|-C] [FILE]...` — sort lines of text files; `-o`, `--compress-program`, and `-T` are rejected (filesystem write / exec)
Expand Down
10 changes: 10 additions & 0 deletions allowedpaths/portable_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ func IsErrIsDirectory(err error) bool {
return errors.Is(err, syscall.EISDIR)
}

// FileIdentity extracts canonical file identity (dev+inode) from FileInfo.
// On Unix, this is extracted directly from Stat_t via info.Sys().
func FileIdentity(_ string, info fs.FileInfo, _ *Sandbox) (uint64, uint64, bool) {
st, ok := info.Sys().(*syscall.Stat_t)
if !ok {
return 0, 0, false
}
return uint64(st.Dev), uint64(st.Ino), true
}

// effectiveHasPerm checks whether the current process has the requested
// permission (writeMask or execMask, each a 3-bit pattern like 0222 or 0111)
// by inspecting the file's owner/group/other permission class that applies to
Expand Down
23 changes: 23 additions & 0 deletions allowedpaths/portable_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package allowedpaths
import (
"errors"
"io/fs"
"os"
"syscall"
)

Comment thread
matt-dz marked this conversation as resolved.
Expand All @@ -21,6 +22,28 @@ func IsErrIsDirectory(err error) bool {
return false
}

// FileIdentity extracts canonical file identity on Windows using
// GetFileInformationByHandle (volume serial + file index).
// The path and sandbox are needed to open the file through the sandbox.
func FileIdentity(absPath string, _ fs.FileInfo, sandbox *Sandbox) (uint64, uint64, bool) {
root, relPath, ok := sandbox.resolve(absPath)
if !ok {
return 0, 0, false
}
f, err := root.OpenFile(relPath, os.O_RDONLY, 0)
if err != nil {
return 0, 0, false
}
defer f.Close()

h := syscall.Handle(f.Fd())
var d syscall.ByHandleFileInformation
if err := syscall.GetFileInformationByHandle(h, &d); err != nil {
return 0, 0, false
}
return uint64(d.VolumeSerialNumber), uint64(d.FileIndexHigh)<<32 | uint64(d.FileIndexLow), true
}

// effectiveHasPerm checks whether the current process has the requested
// permission on Windows. Windows does not use Unix UID/GID permission classes,
// so we fall back to checking any-class bits (0222 / 0111) as before.
Expand Down
41 changes: 41 additions & 0 deletions allowedpaths/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,47 @@ func (s *Sandbox) readDirN(path string, cwd string, maxEntries int) ([]fs.DirEnt
return entries, nil
}

// OpenDir opens a directory within the sandbox for incremental reading
// via ReadDir(n). The caller must close the returned handle when done.
// Returns fs.ReadDirFile to expose only read-only directory methods.
func (s *Sandbox) OpenDir(path string, cwd string) (fs.ReadDirFile, error) {
absPath := toAbs(path, cwd)

root, relPath, ok := s.resolve(absPath)
if !ok {
return nil, &os.PathError{Op: "opendir", Path: path, Err: os.ErrPermission}
}

f, err := root.Open(relPath)
if err != nil {
return nil, PortablePathError(err)
}
return f, nil
}

// IsDirEmpty checks whether a directory is empty by reading at most one
// entry. More efficient than reading all entries when only emptiness
// needs to be determined.
func (s *Sandbox) IsDirEmpty(path string, cwd string) (bool, error) {
absPath := toAbs(path, cwd)

root, relPath, ok := s.resolve(absPath)
if !ok {
return false, &os.PathError{Op: "readdir", Path: path, Err: os.ErrPermission}
}

f, err := root.Open(relPath)
if err != nil {
return false, PortablePathError(err)
}
defer f.Close()
entries, err := f.ReadDir(1)
if err != nil && err != io.EOF {
return false, PortablePathError(err)
}
return len(entries) == 0, nil
}

// ReadDirLimited reads directory entries, skipping the first offset entries
// and returning up to maxRead entries sorted by name within the read window.
// Returns (entries, truncated, error). When truncated is true, the directory
Expand Down
74 changes: 39 additions & 35 deletions allowedsymbols/symbols_allowedpaths.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,39 +17,43 @@ package allowedsymbols
//
// The permanently banned packages (reflect, unsafe) apply here too.
var allowedpathsAllowedSymbols = []string{
"errors.As", // error type assertion; pure function, no I/O.
"errors.Is", // error comparison; pure function, no I/O.
"errors.New", // creates a simple error value; pure function, no I/O.
"fmt.Errorf", // formatted error creation; pure function, no I/O.
"io.EOF", // sentinel error value; pure constant.
"io.ReadWriteCloser", // combined interface type; no side effects.
"io/fs.DirEntry", // interface type for directory entries; no side effects.
"io/fs.ErrExist", // sentinel error for "already exists"; pure constant.
"io/fs.ErrNotExist", // sentinel error for "does not exist"; pure constant.
"io/fs.ErrPermission", // sentinel error for permission denied; pure constant.
"io/fs.FileInfo", // interface type for file metadata; no side effects.
"io/fs.FileMode", // file permission bits type; pure type.
"os.DevNull", // platform null device path constant; pure constant.
"os.ErrPermission", // sentinel error for permission denied; pure constant.
"os.FileMode", // file permission bits type; pure type.
"os.Getgid", // returns the numeric group id of the caller; read-only syscall.
"os.Getgroups", // returns supplementary group ids; read-only syscall.
"os.Getuid", // returns the numeric user id of the caller; read-only syscall.
"os.O_RDONLY", // read-only file flag constant; pure constant.
"os.OpenRoot", // opens a directory as a root for sandboxed file access; needed for sandbox.
"os.PathError", // error type wrapping path and operation; pure type.
"os.Root", // sandboxed directory root type; core of the filesystem sandbox.
"os.Stat", // returns file info for a path; needed for sandbox path validation.
"path/filepath.Abs", // returns absolute path; pure path computation.
"path/filepath.IsAbs", // checks if path is absolute; pure function, no I/O.
"path/filepath.Join", // joins path elements; pure function, no I/O.
"path/filepath.Rel", // returns relative path; pure path computation.
"path/filepath.Separator", // OS path separator constant; pure constant.
"slices.SortFunc", // sorts a slice with a comparison function; pure function, no I/O.
"strings.Compare", // compares two strings lexicographically; pure function, no I/O.
"strings.EqualFold", // case-insensitive string comparison; pure function, no I/O.
"strings.HasPrefix", // pure function for prefix matching; no I/O.
"syscall.EISDIR", // "is a directory" errno constant; pure constant.
"syscall.Errno", // system call error number type; pure type.
"syscall.Stat_t", // file stat structure type; pure type for Unix file metadata.
"errors.As", // error type assertion; pure function, no I/O.
"errors.Is", // error comparison; pure function, no I/O.
"errors.New", // creates a simple error value; pure function, no I/O.
"fmt.Errorf", // formatted error creation; pure function, no I/O.
"io.EOF", // sentinel error value; pure constant.
"io.ReadWriteCloser", // combined interface type; no side effects.
"io/fs.DirEntry", // interface type for directory entries; no side effects.
"io/fs.ErrExist", // sentinel error for "already exists"; pure constant.
"io/fs.ErrNotExist", // sentinel error for "does not exist"; pure constant.
"io/fs.ErrPermission", // sentinel error for permission denied; pure constant.
"io/fs.FileInfo", // interface type for file metadata; no side effects.
"io/fs.FileMode", // file permission bits type; pure type.
"io/fs.ReadDirFile", // read-only directory handle interface; no write capability.
"os.DevNull", // platform null device path constant; pure constant.
"os.ErrPermission", // sentinel error for permission denied; pure constant.
"os.FileMode", // file permission bits type; pure type.
"os.Getgid", // returns the numeric group id of the caller; read-only syscall.
"os.Getgroups", // returns supplementary group ids; read-only syscall.
"os.Getuid", // returns the numeric user id of the caller; read-only syscall.
"os.O_RDONLY", // read-only file flag constant; pure constant.
"os.OpenRoot", // opens a directory as a root for sandboxed file access; needed for sandbox.
"os.PathError", // error type wrapping path and operation; pure type.
"os.Root", // sandboxed directory root type; core of the filesystem sandbox.
"os.Stat", // returns file info for a path; needed for sandbox path validation.
"path/filepath.Abs", // returns absolute path; pure path computation.
"path/filepath.IsAbs", // checks if path is absolute; pure function, no I/O.
"path/filepath.Join", // joins path elements; pure function, no I/O.
"path/filepath.Rel", // returns relative path; pure path computation.
"path/filepath.Separator", // OS path separator constant; pure constant.
"slices.SortFunc", // sorts a slice with a comparison function; pure function, no I/O.
"strings.Compare", // compares two strings lexicographically; pure function, no I/O.
"strings.EqualFold", // case-insensitive string comparison; pure function, no I/O.
"strings.HasPrefix", // pure function for prefix matching; no I/O.
"syscall.ByHandleFileInformation", // Windows file identity structure; pure type for file metadata.
"syscall.EISDIR", // "is a directory" errno constant; pure constant.
"syscall.Errno", // system call error number type; pure type.
"syscall.GetFileInformationByHandle", // Windows API for file identity (vol serial + file index); read-only syscall.
"syscall.Handle", // Windows file handle type; pure type alias.
"syscall.Stat_t", // file stat structure type; pure type for Unix file metadata.
}
Loading
Loading