diff options
| -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 | ||||
| -rw-r--r-- | integrationtests/dir_test.go | 16 | ||||
| -rw-r--r-- | integrationtests/priority_test.go | 30 |
5 files changed, 134 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) { diff --git a/integrationtests/dir_test.go b/integrationtests/dir_test.go index d759abd..bbcc3fe 100644 --- a/integrationtests/dir_test.go +++ b/integrationtests/dir_test.go @@ -24,6 +24,22 @@ func TestDirMkdirat(t *testing.T) { }) } +// TestDirMknodatFifo verifies mknodat(2) is traced end-to-end. The +// dir-mknodat-fifo workload creates an unprivileged FIFO node under AT_FDCWD, +// so enter_mknodat fires with pathname@args[1] (after dirfd@args[0]). Matching +// the distinct fifo name via PathContains proves the args[1] capture, mirroring +// the mkdirat coverage above. +func TestDirMknodatFifo(t *testing.T) { + runScenario(t, "dir-mknodat-fifo", []ExpectedEvent{ + { + PathContains: "mknodat-fifo", + Tracepoint: "enter_mknodat", + Comm: "ioworkload", + MinCount: 1, + }, + }) +} + func TestDirChdir(t *testing.T) { runScenario(t, "dir-chdir", []ExpectedEvent{ { diff --git a/integrationtests/priority_test.go b/integrationtests/priority_test.go index 9e41185..670aa8c 100644 --- a/integrationtests/priority_test.go +++ b/integrationtests/priority_test.go @@ -31,3 +31,33 @@ func TestPriorityBasic(t *testing.T) { {Tracepoint: "enter_setpriority", Comm: "ioworkload", MinCount: 1}, }) } + +// ioprioTraceArgs restricts tracing to the two I/O-priority syscalls the +// ioprio-basic workload issues. Each tracepoint is named after the underlying +// kernel syscall, so the names below match verbatim. +var ioprioTraceArgs = []string{"-trace-syscalls", "ioprio_get,ioprio_set"} + +// TestIoprioBasic verifies the ioprio_get/ioprio_set pair (the I/O-priority +// analogues of getpriority/setpriority) is traced end-to-end. The ioprio-basic +// workload self-targets both calls (IOPRIO_WHO_PROCESS, who 0 == the calling +// process): it reads the current I/O priority with ioprio_get and re-applies it +// (or a harmless best-effort default when none is set) with ioprio_set, so no +// other process is affected and no privilege is required. Both syscalls classify +// as FamilyProcess with a KindNull enter (IOPRIO_WHO_PROCESS is an opcode, not an +// fd) and an UNCLASSIFIED return (the value is an I/O-priority word, not a byte +// count), so asserting the enter tracepoints appear — attributed to the +// ioworkload process — is the right end-to-end check. +func TestIoprioBasic(t *testing.T) { + h := newTestHarness(t) + result, pid, err := h.RunWithIorArgs("ioprio-basic", defaultDuration, ioprioTraceArgs) + if err != nil { + t.Fatalf("run scenario ioprio-basic: %v", err) + } + + AssertNoUnexpectedPID(t, result, pid) + AssertNoUnexpectedComm(t, result, "ioworkload") + AssertEventsPresent(t, result, []ExpectedEvent{ + {Tracepoint: "enter_ioprio_get", Comm: "ioworkload", MinCount: 1}, + {Tracepoint: "enter_ioprio_set", Comm: "ioworkload", MinCount: 1}, + }) +} |
