summaryrefslogtreecommitdiff
path: root/cmd/ioworkload
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-06 10:12:57 +0300
committerPaul Buetow <paul@buetow.org>2026-06-06 10:12:57 +0300
commit57615945e6796950e7095a3ee8a97651ae3f1bd9 (patch)
tree5593e97a4c45a35755eb1ca2b726583f881380c6 /cmd/ioworkload
parent3ce0f52a9f608b28c550083574fa3ef442107f53 (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.go28
-rw-r--r--cmd/ioworkload/scenario_priority.go58
-rw-r--r--cmd/ioworkload/scenarios.go2
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) {