Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion SHELL_FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ or privileged-helper protocol change is needed.
- ✅ `tr [-cdsCt] SET1 [SET2]` — translate, squeeze, and/or delete characters from stdin
- ✅ `truncate -s SIZE [-c] [FILE]...` — shrink or extend file size; **remediation mode only**, target must be within a `:rw` `AllowedPaths` root; SIZE supports GNU suffix grammar (K/k/KiB/kiB=1024, KB/kB=1000, M/G/T similarly, P/E uppercase-only); relative-size modifiers and `--reference`/`--io-blocks` are rejected; hard-linked targets (link count > 1) are rejected with `hard links are not supported as write targets`
- ✅ `true` — return exit code 0
- ✅ `uname [-asnrvm]` — print system information (Linux only; reads from `/proc/sys/kernel/`, respects `--proc-path`)
- ✅ `uname [-asnrvm]` — print system information (Linux only; reads from `/proc/sys/kernel/`, respects `--proc-path`; `-m` falls back to `uname(2)` on kernels < 6.1 without `/proc/sys/kernel/arch`)
- ✅ `uniq [OPTION]... [INPUT]` — report or omit repeated lines
- ✅ `uptime [-ps]` — tell how long the system has been running; shows current time, uptime duration, and load averages (1/5/15 min); user count omitted (privacy); `-p` prints human-readable duration; `-s` prints boot time as `YYYY-MM-DD HH:MM:SS`; load average unavailable on Windows
- ✅ `vmstat [-a] [-w] [-S k|K|m|M] [-s] [delay count]` — report virtual memory, swap, IO, and CPU pressure statistics; no arguments prints a since-boot snapshot, positive `delay count` samples for at most 29 seconds of total wait time, and `-s`/`--stats` prints a full counter summary; `-S` scales memory and swap-rate columns; reads `/proc/*` (Linux) or `sysctl(3)` (macOS) directly, bypassing `AllowedPaths` (same exception as `df`/`ss`/`ip route`); macOS lacks several counters (Mach-only), shown as `-` rather than fabricated; `-d`, `-p`, `-f`, `-m`, `-t`, `-n`, `-V` are not implemented; not supported on Windows
Expand Down
7 changes: 7 additions & 0 deletions analysis/symbols_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,18 +167,23 @@ var internalPerPackageSymbols = map[string][]string{
"strconv.ParseInt", // 🟢 string-to-int conversion with base/bit-size; pure function, no I/O.
},
"procsyskernel": {
"errors.Is", // 🟢 detects a missing sys/kernel/arch (os.ErrNotExist) before falling back to uname(2); pure function, no I/O.
"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.
"os.ErrNotExist", // 🟢 sentinel error for a missing kernel pseudo-file; read-only constant, no I/O.
"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.
"golang.org/x/sys/unix.ByteSliceToString", // 🟢 (linux) converts uname(2)'s NUL-terminated machine field to a Go string; pure function, no I/O.
"golang.org/x/sys/unix.Uname", // 🟠 (linux) read-only uname(2) fallback for the machine name on kernels < 6.1 without sys/kernel/arch; no write or exec capability.
"golang.org/x/sys/unix.Utsname", // 🟢 (linux) struct type carrying uname(2) data; pure data type.
},
"procnetroute": {
"bufio.NewScanner", // 🟢 line-by-line reading of /proc/net/route; no write capability.
Expand Down Expand Up @@ -528,6 +533,8 @@ var internalAllowedSymbols = []string{
"golang.org/x/sys/unix.KinfoProc", // 🟢 procinfo (darwin): struct type carrying per-process kinfo_proc data from sysctl; read-only data, no exec capability.
"golang.org/x/sys/unix.Major", // 🟢 procfd (linux): extracts the major device number from a raw dev_t; pure function, no I/O.
"golang.org/x/sys/unix.Minor", // 🟢 procfd (linux): extracts the minor device number from a raw dev_t; pure function, no I/O.
"golang.org/x/sys/unix.Uname", // 🟠 procsyskernel (linux): read-only uname(2) fallback for the machine name when sys/kernel/arch is absent (kernels < 6.1); no write or exec capability.
"golang.org/x/sys/unix.Utsname", // 🟢 procsyskernel (linux): struct type carrying uname(2) data; pure data type.
"golang.org/x/sys/unix.MNT_LOCAL", // 🟢 diskstats (darwin): flag constant indicating a local-only filesystem; pure constant.
"golang.org/x/sys/unix.MNT_NOWAIT", // 🟢 diskstats (darwin): flag constant: do not block on remote FS for getfsstat; pure constant.
"golang.org/x/sys/unix.S_IFBLK", // 🟢 procfd (linux): file-type bitmask constant for block devices; pure constant.
Expand Down
29 changes: 29 additions & 0 deletions builtins/internal/procsyskernel/machine_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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

import (
"errors"
"os"

"golang.org/x/sys/unix"
)

// ReadMachine returns the machine hardware name (uname -m). It reads
// sys/kernel/arch, which Linux only provides since 6.1. When that file is
// absent (e.g. RHEL 9's 5.14 kernel) it falls back to the machine field of
// uname(2), the same source GNU uname uses. Any other read error is returned.
func ReadMachine(procPath string) (string, error) {
machine, err := ReadFile(procPath, "arch")
if !errors.Is(err, os.ErrNotExist) {
return machine, err
}
var uts unix.Utsname
if unameErr := unix.Uname(&uts); unameErr != nil {
return "", err
}
return unix.ByteSliceToString(uts.Machine[:]), nil
}
14 changes: 14 additions & 0 deletions builtins/internal/procsyskernel/machine_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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.

//go:build !linux

package procsyskernel

// ReadMachine returns the machine hardware name (uname -m) from
// sys/kernel/arch.
func ReadMachine(procPath string) (string, error) {
return ReadFile(procPath, "arch")
}
7 changes: 7 additions & 0 deletions builtins/proc_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,10 @@ func (p *ProcProvider) ListOpenFiles(ctx context.Context, pids []int, filter pro
func (p *ProcProvider) ReadKernelFile(name string) (string, error) {
return procsyskernel.ReadFile(p.path, name)
}

// ReadMachine returns the machine hardware name for uname -m. It reads
// sys/kernel/arch and, on Linux kernels older than 6.1 that lack that file,
// falls back to uname(2).
func (p *ProcProvider) ReadMachine() (string, error) {
return procsyskernel.ReadMachine(p.path)
}
33 changes: 33 additions & 0 deletions builtins/tests/uname/uname_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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 uname_test

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)

// Linux only provides /proc/sys/kernel/arch since 6.1 (e.g. RHEL 9's 5.14
// kernel lacks it); -m must then fall back to uname(2) rather than fail.
func TestUnameMachineFallsBackWithoutArchFile(t *testing.T) {
requireLinux(t)
dir := t.TempDir()
vals := defaultFakeProc()
delete(vals, "arch")
writeFakeProc(t, dir, vals)

var uts unix.Utsname
require.NoError(t, unix.Uname(&uts))
machine := unix.ByteSliceToString(uts.Machine[:])
require.NotEmpty(t, machine)

stdout, stderr, code := cmdRun(t, "uname -m; uname -a", dir)
assert.Equal(t, 0, code, stderr)
assert.Equal(t, machine+"\nLinux testhost 5.15.0-test #1 SMP Test "+machine+"\n", stdout)
}
12 changes: 9 additions & 3 deletions builtins/uname/uname.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
// -n sys/kernel/hostname
// -r sys/kernel/osrelease
// -v sys/kernel/version
// -m sys/kernel/arch
// -m sys/kernel/arch (Linux >= 6.1), falling back to uname(2) when absent
//
// Exit codes:
//
Expand Down Expand Up @@ -65,7 +65,7 @@ var kernelFiles = [...]struct {
{"n", "nodename", "hostname"},
{"r", "kernel-release", "osrelease"},
{"v", "kernel-version", "version"},
{"m", "machine", "arch"}, // Available since Linux 2.6 (2003); last 2.6 LTS (2.6.32) EOL Feb 2016.
{"m", "machine", "arch"}, // Only present since Linux 6.1; see ProcProvider.ReadMachine.
}

func makeFlags(fs *builtins.FlagSet) builtins.HandlerFunc {
Expand Down Expand Up @@ -128,7 +128,13 @@ func makeFlags(fs *builtins.FlagSet) builtins.HandlerFunc {
if ctx.Err() != nil {
return builtins.Result{Code: 1}
}
val, err := callCtx.Proc.ReadKernelFile(entry.file)
var val string
var err error
if entry.short == "m" {
val, err = callCtx.Proc.ReadMachine()
} else {
val, err = callCtx.Proc.ReadKernelFile(entry.file)
}
if err != nil {
callCtx.Errf("uname: cannot read %s: %s\n", entry.file, err)
return builtins.Result{Code: 1}
Expand Down
6 changes: 5 additions & 1 deletion cmd/rshell/privileged_worker_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,14 @@ func trustedPathsForCommands(allowedCommands []string) []sandboxlandlock.Trusted
"/proc/sys/kernel/hostname",
"/proc/sys/kernel/osrelease",
"/proc/sys/kernel/version",
"/proc/sys/kernel/arch",
} {
trusted = append(trusted, trustedReadOnlyFile(path))
}
// arch only exists since Linux 6.1 (absent on e.g. RHEL 9); uname
// falls back to uname(2) without it.
arch := trustedReadOnlyFile("/proc/sys/kernel/arch")
arch.Optional = true
trusted = append(trusted, arch)
}
return trusted
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/rshell/privileged_worker_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func TestTrustedPathsForCommands(t *testing.T) {
trustedReadOnlyFile("/proc/sys/kernel/hostname"),
trustedReadOnlyFile("/proc/sys/kernel/osrelease"),
trustedReadOnlyFile("/proc/sys/kernel/version"),
trustedReadOnlyFile("/proc/sys/kernel/arch"),
{Path: "/proc/sys/kernel/arch", Kind: sandboxlandlock.TrustedPathFile, Access: sandboxlandlock.TrustedPathReadOnly, Optional: true},
}, paths)
}

Expand Down
2 changes: 1 addition & 1 deletion docs/PRIVILEGED_HELPER.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ corresponding command is in the verified effective command allowlist:
| `rshell:ps` | `/proc` hierarchy |
| `rshell:ss`, `rshell:ip` | `/proc/net` hierarchy |
| `rshell:df` | exact file `/proc/self/mountinfo` |
| `rshell:uname` | exact files `/proc/sys/kernel/{ostype,hostname,osrelease,version,arch}` |
| `rshell:uname` | exact files `/proc/sys/kernel/{ostype,hostname,osrelease,version}`, plus `/proc/sys/kernel/arch` when present (Linux < 6.1, e.g. RHEL 9, lacks it; `uname -m` then uses `uname(2)`) |

The complete fixed set is granted for an allowed builtin because shell
expansion can choose its flags at runtime. Systemd-aware builtins similarly use
Expand Down
18 changes: 18 additions & 0 deletions internal/sandbox/landlock/restrict_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package landlock
import (
"os"
"path/filepath"
"syscall"
"testing"

ll "github.com/landlock-lsm/go-landlock/landlock/syscall"
Expand Down Expand Up @@ -57,6 +58,23 @@ func TestOpenRulesSupportsExactFileAndOptionalDirectory(t *testing.T) {
require.Equal(t, devNullAccess, rules[1].allowedAccess)
}

func TestOpenRulesSkipsOnlyOptionalMissingExactFile(t *testing.T) {
missing := filepath.Join(t.TempDir(), "missing-file")

rules, err := openRules(nil, []TrustedPath{
{Path: missing, Kind: TrustedPathFile, Access: TrustedPathReadOnly, Optional: true},
})
require.NoError(t, err)
defer closeOpenedRules(rules)
require.Len(t, rules, 1)
require.Equal(t, "/dev/null", rules[0].path)

_, err = openRules(nil, []TrustedPath{
{Path: missing, Kind: TrustedPathFile, Access: TrustedPathReadOnly},
})
require.ErrorIs(t, err, syscall.ENOENT)
}

func TestOpenRulesRejectsRemoveGrantForExactFile(t *testing.T) {
file := filepath.Join(t.TempDir(), "file")
require.NoError(t, os.WriteFile(file, []byte("data"), 0o600))
Expand Down
Loading