Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1f2f188
feat(uname): implement uname builtin (Linux only, proc-based)
matt-dz Mar 25, 2026
2c6a0c3
refactor(uname): use ProcProvider.ReadKernelFile instead of raw os calls
matt-dz Mar 25, 2026
16d1026
fix(uname): show clear platform error on non-Linux systems
matt-dz Mar 25, 2026
6244031
test(uname): add comprehensive unit and scenario tests
matt-dz Mar 25, 2026
69d2faa
fix(uname): reject extra positional operands
matt-dz Mar 25, 2026
1bd4097
fix(proc): bound ReadKernelFile reads to 4 KiB
matt-dz Mar 25, 2026
a90396b
fix(test): map context cancellation to non-zero exit in runScriptCtx
matt-dz Mar 25, 2026
4a6940b
fix(proc): reject non-regular files in ReadKernelFile
matt-dz Mar 25, 2026
7bf18fa
fix(proc): stat before open in ReadKernelFile to prevent FIFO hang
matt-dz Mar 25, 2026
70d0acd
fix(proc): atomic open+fstat in ReadKernelFile to close TOCTOU race
matt-dz Mar 25, 2026
98f6320
refactor(uname): move kernel reads to builtins/internal/procsyskernel
matt-dz Mar 26, 2026
8f3aad1
fix(procsyskernel): reject ".." in procPath for defence-in-depth
matt-dz Mar 26, 2026
f1a0208
fix(uname): use GNU-compatible long flag names instead of one-letter
matt-dz Mar 26, 2026
251be0f
fix(procsyskernel): validate name is a plain basename
matt-dz Mar 26, 2026
0b2917e
fix(uname): match GNU error format and enable bash comparison
matt-dz Mar 26, 2026
c02f36e
fix(uname): fall back to runtime.GOARCH for -m when proc file missing
matt-dz Mar 26, 2026
bb3c3b8
revert(uname): remove runtime.GOARCH fallback for -m
matt-dz Mar 26, 2026
3a96e33
docs(uname): note /proc/sys/kernel/arch availability since Linux 2.6
matt-dz Mar 26, 2026
c3a1e11
fix(uname): include long option names in --help output
matt-dz Mar 26, 2026
2407658
docs(test): add comments explaining skip_assert_against_bash in uname…
matt-dz Mar 26, 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 @@ -31,6 +31,7 @@ Blocked features are rejected before execution with exit code 2.
- ✅ `test EXPRESSION` / `[ EXPRESSION ]` — evaluate conditional expression (file tests, string/integer comparison, logical operators)
- ✅ `tr [-cdsCt] SET1 [SET2]` — translate, squeeze, and/or delete characters from stdin
- ✅ `true` — return exit code 0
- ✅ `uname [-asnrvm]` — print system information (Linux only; reads from `/proc/sys/kernel/`, respects `--proc-path`)
- ✅ `uniq [OPTION]... [INPUT]` — report or omit repeated lines
- ✅ `wc [-l] [-w] [-c] [-m] [-L] [FILE]...` — count lines, words, bytes, characters, or max line length
- ❌ All other commands — return exit code 127 with `<cmd>: not found` unless an ExecHandler is configured
Expand Down
5 changes: 5 additions & 0 deletions allowedsymbols/symbols_builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,11 @@ var builtinPerCommandSymbols = map[string][]string{
"true": {
"context.Context", // 🟢 deadline/cancellation plumbing; pure interface, no side effects.
},
"uname": {
"context.Context", // 🟢 deadline/cancellation plumbing; pure interface, no side effects.
"runtime.GOOS", // 🟢 current OS name constant; pure constant, no I/O.
"strings.Join", // 🟢 joins string slices; pure function, no I/O.
},
"uniq": {
"bufio.NewScanner", // 🟢 line-by-line input reading (e.g. head, cat); no write or exec capability.
"bufio.SplitFunc", // 🟢 type for custom scanner split functions; pure type, no I/O.
Expand Down
21 changes: 21 additions & 0 deletions allowedsymbols/symbols_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ var internalPerPackageSymbols = map[string][]string{
"procpath": {
// No stdlib symbols needed — this package only defines a string constant.
},
"procsyskernel": {
"fmt.Errorf", // 🟢 error formatting; pure function, no I/O.
"io.LimitReader", // 🟢 wraps a reader with a byte cap; pure wrapper, no I/O by itself.
"io.ReadAll", // 🟠 reads all data from a reader; bounded by io.LimitReader.
"os.ModeCharDevice", // 🟢 file mode constant; pure constant.
"os.O_RDONLY", // 🟢 read-only file flag; pure constant.
"os.OpenFile", // 🟠 opens kernel pseudo-files for reading; bypasses AllowedPaths by design.
"path/filepath.Base", // 🟢 returns the last element of a path; validates name is a plain basename.
"path/filepath.Clean", // 🟢 normalises path before use; pure function, no I/O.
"path/filepath.Join", // 🟢 joins path elements; pure function, no I/O.
"strings.Contains", // 🟢 checks for ".." traversal in procPath; pure function, no I/O.
"strings.TrimRight", // 🟢 trims trailing characters; pure function, no I/O.
"syscall.O_NONBLOCK", // 🟢 non-blocking open flag; prevents FIFO hang. Pure constant.
},
"procnetroute": {
"bufio.NewScanner", // 🟢 line-by-line reading of /proc/net/route; no write capability.
"github.com/DataDog/rshell/builtins/internal/procpath.Default", // 🟢 canonical /proc filesystem root path constant; pure constant, no I/O.
Expand Down Expand Up @@ -128,11 +142,17 @@ var internalAllowedSymbols = []string{
"fmt.Errorf", // 🟢 error formatting; pure function, no I/O.
"os.ErrNotExist", // 🟢 procinfo: sentinel error value indicating a file or directory does not exist; read-only constant, no I/O.
"fmt.Sprintf", // 🟢 string formatting; pure function, no I/O.
"io.LimitReader", // 🟢 procsyskernel: wraps a reader with a byte cap; pure wrapper, no I/O by itself.
"io.ReadAll", // 🟠 procsyskernel: reads all data from a bounded reader; used with LimitReader for 4KiB cap.
"os.Getpid", // 🟠 procinfo: returns the current process ID; read-only, no side effects.
"os.ModeCharDevice", // 🟢 procsyskernel: file mode constant for char device detection; pure constant.
"os.O_RDONLY", // 🟢 procsyskernel: read-only open flag; pure constant.
"os.Open", // 🟠 procinfo: opens a file read-only; needed to stream /proc/stat line-by-line.
"os.OpenFile", // 🟠 procsyskernel: opens kernel pseudo-files with O_NONBLOCK; bypasses AllowedPaths by design.
"os.ReadDir", // 🟠 procinfo: reads a directory listing; needed to enumerate /proc entries.
"os.ReadFile", // 🟠 procinfo: reads a whole file; needed to read /proc/[pid]/{stat,cmdline,status}.
"os.Stat", // 🟠 procinfo: validates that the proc path exists before enumeration; read-only metadata, no write capability.
"path/filepath.Base", // 🟢 procsyskernel: returns the last element of a path; validates name is a plain basename.
"path/filepath.Clean", // 🟢 procnetroute/procnetsocket: normalises procPath before ".." safety check; pure function, no I/O.
"path/filepath.Join", // 🟢 procinfo: joins path elements to construct /proc/<pid>/stat paths; pure function, no I/O.
"strconv.Atoi", // 🟢 string-to-int conversion; pure function, no I/O.
Expand All @@ -153,6 +173,7 @@ var internalAllowedSymbols = []string{
"strings.TrimSpace", // 🟢 procinfo: removes leading/trailing whitespace; pure function, no I/O.
"syscall.Errno", // 🟢 winnet: wraps DLL return code as an error type; pure type, no I/O.
"syscall.Getsid", // 🟠 procinfo: returns the session ID of a process; read-only syscall, no write/exec.
"syscall.O_NONBLOCK", // 🟢 procsyskernel: non-blocking open flag to prevent FIFO hang; pure constant.
"syscall.MustLoadDLL", // 🔴 winnet: loads iphlpapi.dll once at program init; read-only OS loader call.
"syscall.Proc", // 🟢 winnet: DLL procedure handle type used in function signature; pure type, no I/O.
"time.Now", // 🟠 procinfo: returns the current wall-clock time; read-only, no side effects.
Expand Down
72 changes: 72 additions & 0 deletions builtins/internal/procsyskernel/procsyskernel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2026-present Datadog, Inc.

// Package procsyskernel reads Linux kernel information from /proc/sys/kernel/.
//
// This package is in builtins/internal/ and is therefore exempt from the
// builtinAllowedSymbols allowlist check. It may use OS-specific APIs freely.
//
// # Sandbox bypass
//
// ReadFile intentionally bypasses the AllowedPaths sandbox (callCtx.OpenFile)
// and calls os.OpenFile directly. This is safe because procPath is always a
// kernel-managed pseudo-filesystem root (/proc by default) that is hardcoded
// by the caller — it is never derived from user-supplied input and cannot be
// redirected by a shell script. The caller is responsible for ensuring that
// procPath remains a safe, non-user-controlled path.
package procsyskernel

import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
)

// ReadFile reads a single-line value from a /proc/sys/kernel/ pseudo-file.
// name is the filename (e.g. "ostype", "hostname"). procPath is the base
// proc path (e.g. "/proc" or "/host/proc").
//
// The file is opened with O_NONBLOCK to prevent blocking on FIFOs, then
// validated via fstat to reject non-regular files. Reads are bounded to
// 4 KiB. The returned value is trimmed of trailing whitespace.
func ReadFile(procPath, name string) (string, error) {
// Defence-in-depth: reject ".." in the original path before Clean
// so traversal like "/proc/../etc/passwd" is caught. Matches the
// equivalent guard in procnetroute and procnetsocket.
if strings.Contains(procPath, "..") {
return "", fmt.Errorf("procsyskernel: unsafe procPath %q (must not contain \"..\" components)", procPath)
}
// Reject path components in name — must be a plain basename (e.g. "ostype").
if name != filepath.Base(name) || strings.Contains(name, "..") {
return "", fmt.Errorf("procsyskernel: unsafe name %q (must be a plain filename)", name)
}
path := filepath.Join(filepath.Clean(procPath), "sys", "kernel", name)
// Open with O_NONBLOCK to prevent blocking on FIFOs, then validate
// the file type via fstat on the opened fd. This is atomic — no
// TOCTOU gap between type check and open.
f, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NONBLOCK, 0)
Comment thread
matt-dz marked this conversation as resolved.
if err != nil {
return "", err
}
defer f.Close()
info, err := f.Stat()
if err != nil {
return "", err
}
if !info.Mode().IsRegular() && info.Mode().Type()&os.ModeCharDevice == 0 {
// Allow regular files and char devices (proc pseudo-files appear as
// char devices on some configurations). Reject FIFOs, sockets, etc.
return "", fmt.Errorf("not a regular file: %s", path)
}
// Proc kernel files are tiny single-line values. Cap at 4 KiB.
data, err := io.ReadAll(io.LimitReader(f, 4096))
if err != nil {
return "", err
}
return strings.TrimRight(string(data), " \t\r\n"), nil
}
13 changes: 13 additions & 0 deletions builtins/proc_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"context"

"github.com/DataDog/rshell/builtins/internal/procinfo"
"github.com/DataDog/rshell/builtins/internal/procsyskernel"
)

// ProcProvider gives builtins controlled access to the proc filesystem.
Expand All @@ -26,6 +27,11 @@ func NewProcProvider(path string) *ProcProvider {
return &ProcProvider{path: path}
}

// ProcPath returns the configured proc filesystem path (e.g. "/proc" or "/host/proc").
func (p *ProcProvider) ProcPath() string {
return p.path
}

// ListAll returns all running processes.
func (p *ProcProvider) ListAll(ctx context.Context) ([]procinfo.ProcInfo, error) {
return procinfo.ListAll(ctx, p.path)
Expand All @@ -40,3 +46,10 @@ func (p *ProcProvider) GetSession(ctx context.Context) ([]procinfo.ProcInfo, err
func (p *ProcProvider) GetByPIDs(ctx context.Context, pids []int) ([]procinfo.ProcInfo, error) {
return procinfo.GetByPIDs(ctx, p.path, pids)
}

// ReadKernelFile reads a single-line value from a /proc/sys/kernel/ pseudo-file.
// name is the filename relative to sys/kernel/ (e.g. "ostype", "hostname").
// The returned value is trimmed of trailing whitespace.
func (p *ProcProvider) ReadKernelFile(name string) (string, error) {
Comment thread
matt-dz marked this conversation as resolved.
return procsyskernel.ReadFile(p.path, name)
}
Loading
Loading