diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-06 10:12:57 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-06 10:12:57 +0300 |
| commit | 57615945e6796950e7095a3ee8a97651ae3f1bd9 (patch) | |
| tree | 5593e97a4c45a35755eb1ca2b726583f881380c6 /cmd/ioworkload | |
| parent | 3ce0f52a9f608b28c550083574fa3ef442107f53 (diff) | |
test: add coverage for mknodat and ioprio_get/ioprio_set
Add end-to-end integration scenarios and tests for two previously
untested syscalls:
- mknodat(2): new dir-mknodat-fifo scenario creates an unprivileged
FIFO node (S_IFIFO, no CAP_MKNOD) via unix.Mknodat under AT_FDCWD
and unlinks it. TestDirMknodatFifo asserts enter_mknodat fires with
pathname@args[1] (after dirfd@args[0]), proven by a PathContains
match on the distinct fifo name, mirroring the mkdirat coverage.
- ioprio_get(2)/ioprio_set(2): new ioprio-basic scenario (the
I/O-priority analogues of getpriority/setpriority) issues the raw
syscalls (no x/sys wrapper exists), reading the current self I/O
priority and re-applying it, or a harmless unprivileged best-effort
value when none is set. TestIoprioBasic asserts enter_ioprio_get and
enter_ioprio_set fire (null enters, UNCLASSIFIED ret), mirroring
priority-basic. Realtime class is deliberately avoided as it needs
CAP_SYS_ADMIN.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'cmd/ioworkload')
| -rw-r--r-- | cmd/ioworkload/scenario_dir.go | 28 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_priority.go | 58 | ||||
| -rw-r--r-- | cmd/ioworkload/scenarios.go | 2 |
3 files changed, 88 insertions, 0 deletions
diff --git a/cmd/ioworkload/scenario_dir.go b/cmd/ioworkload/scenario_dir.go index 7a78716..9faa31e 100644 --- a/cmd/ioworkload/scenario_dir.go +++ b/cmd/ioworkload/scenario_dir.go @@ -8,6 +8,8 @@ import ( "syscall" "time" "unsafe" + + "golang.org/x/sys/unix" ) // dirBasic creates a directory via raw SYS_MKDIR, checks access, then removes it @@ -63,6 +65,32 @@ func dirMkdirat() error { return nil } +// dirMknodatFifo creates a FIFO (named pipe) via mknodat(2) using +// unix.Mknodat with AT_FDCWD. A FIFO node (S_IFIFO) is unprivileged to +// create: unlike character/block device nodes it needs no CAP_MKNOD, so the +// scenario runs as an ordinary user. mknodat captures pathname@args[1] (after +// dirfd@args[0]); a PathContains match on the distinct fifo name proves the +// args[1] capture fired. The FIFO is removed afterward. +func dirMknodatFifo() error { + dir, cleanup, err := makeTempDir("dir-mknodat") + if err != nil { + return err + } + defer cleanup() + + fifoPath := filepath.Join(dir, "mknodat-fifo") + // S_IFIFO | 0o600: a FIFO node, owner read/write. dev is ignored for FIFOs. + if err := unix.Mknodat(unix.AT_FDCWD, fifoPath, syscall.S_IFIFO|0o600, 0); err != nil { + return fmt.Errorf("mknodat fifo: %w", err) + } + // RemoveAll on the temp dir (via cleanup) also removes the FIFO, but unlink + // it explicitly so the node lifetime matches the scenario's intent. + if err := syscall.Unlink(fifoPath); err != nil { + return fmt.Errorf("unlink fifo: %w", err) + } + return nil +} + // dirChdir creates a temp directory, then changes to it via chdir(2). // Restores the original working directory afterward. func dirChdir() error { diff --git a/cmd/ioworkload/scenario_priority.go b/cmd/ioworkload/scenario_priority.go index 1383376..548df1b 100644 --- a/cmd/ioworkload/scenario_priority.go +++ b/cmd/ioworkload/scenario_priority.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "syscall" "golang.org/x/sys/unix" ) @@ -55,3 +56,60 @@ func priorityBasic() error { } return nil } + +// ioprioWhoProcess is IOPRIO_WHO_PROCESS (value 1): the "which" selector telling +// ioprio_get/ioprio_set that "who" identifies a single process (rather than a +// process group or user). Paired with who == 0 it means "the calling process", +// so the calls are entirely self-targeted and need no privilege. +const ioprioWhoProcess = 1 + +// ioprioClassShift is the bit position of the I/O-priority class within the +// 16-bit ioprio value: the top 3 bits hold the class, the low 13 bits the level. +// IOPRIO_PRIO_VALUE(class, level) == (class << ioprioClassShift) | level. +const ioprioClassShift = 13 + +// ioprioClassBE is IOPRIO_CLASS_BE (value 2), the best-effort scheduling class. +// Setting best-effort on SELF is unprivileged; the realtime class +// (IOPRIO_CLASS_RT == 1) would require CAP_SYS_ADMIN, so we never use it. +const ioprioClassBE = 2 + +// ioprioBasic exercises the SAFE, NON-DISRUPTIVE I/O-priority pair +// (ioprio_get/ioprio_set), the I/O analogues of getpriority/setpriority. There +// is no x/sys wrapper for these, so we issue the raw syscalls directly. Both are +// self-targeted (IOPRIO_WHO_PROCESS, who 0 == the calling process), so they +// affect no other process: +// +// - ioprio_get(IOPRIO_WHO_PROCESS, 0) READS the current I/O priority. The +// return is a packed (class<<13)|level value, or 0 when none is set (the +// process inherits a default derived from its CPU nice value). +// - ioprio_set(IOPRIO_WHO_PROCESS, 0, value) re-applies the value just read. +// If ioprio_get returned 0 (no explicit priority), we instead set best-effort +// level 4 — a harmless, unprivileged choice. Best-effort needs no privilege; +// we deliberately avoid the realtime class, which needs CAP_SYS_ADMIN. +// +// Both syscalls classify as FamilyProcess with a KindNull enter (IOPRIO_WHO_* +// is an opcode, not an fd) and an UNCLASSIFIED return (the value is an +// I/O-priority word, NOT a byte count), so the scenario exists purely to fire +// the enter tracepoints end-to-end, mirroring priorityBasic above. +func ioprioBasic() error { + // ioprio_get returns -1/errno on failure, else the packed priority (>= 0). + ret, _, errno := syscall.Syscall(unix.SYS_IOPRIO_GET, ioprioWhoProcess, 0, 0) + if errno != 0 { + return fmt.Errorf("ioprio_get(IOPRIO_WHO_PROCESS, 0): %w", errno) + } + + ioprioValue := uintptr(ret) + if ioprioValue == 0 { + // No explicit I/O priority is set; choose a harmless best-effort value + // (class BE, level 4) so ioprio_set has a valid, unprivileged argument. + ioprioValue = uintptr(ioprioClassBE<<ioprioClassShift | 4) + } + + // Re-apply the value we just read (or the safe best-effort default): a + // non-disruptive, unprivileged self-update. + _, _, errno = syscall.Syscall(unix.SYS_IOPRIO_SET, ioprioWhoProcess, 0, ioprioValue) + if errno != 0 { + return fmt.Errorf("ioprio_set(IOPRIO_WHO_PROCESS, 0, %#x): %w", ioprioValue, errno) + } + return nil +} diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go index 2eb18aa..9cfda58 100644 --- a/cmd/ioworkload/scenarios.go +++ b/cmd/ioworkload/scenarios.go @@ -92,6 +92,7 @@ var scenarios = map[string]func() error{ "unlink-unlinkat-enoent": unlinkUnlinkatEnoent, "dir-basic": dirBasic, "dir-mkdirat": dirMkdirat, + "dir-mknodat-fifo": dirMknodatFifo, "dir-chdir": dirChdir, "dir-getcwd": dirGetcwd, "dir-getdents": dirGetdents, @@ -166,6 +167,7 @@ var scenarios = map[string]func() error{ "misc-basic": miscBasic, "sched-basic": schedBasic, "priority-basic": priorityBasic, + "ioprio-basic": ioprioBasic, } func makeTempDir(prefix string) (string, func(), error) { |
