summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/ioworkload/scenario_ioctl.go48
-rw-r--r--cmd/ioworkload/scenario_mountfs.go12
-rw-r--r--cmd/ioworkload/scenarios.go1
-rw-r--r--integrationtests/ioctl_test.go17
-rw-r--r--integrationtests/mountfs_test.go6
5 files changed, 83 insertions, 1 deletions
diff --git a/cmd/ioworkload/scenario_ioctl.go b/cmd/ioworkload/scenario_ioctl.go
new file mode 100644
index 0000000..ad7ed7d
--- /dev/null
+++ b/cmd/ioworkload/scenario_ioctl.go
@@ -0,0 +1,48 @@
+package main
+
+import (
+ "fmt"
+ "path/filepath"
+ "syscall"
+
+ "golang.org/x/sys/unix"
+)
+
+// fionread is the FIONREAD ioctl request (number of bytes available to read).
+// golang.org/x/sys/unix does not export it as a portable constant, so we define
+// it here. The value 0x541B is shared by the architectures this project targets
+// (amd64, arm64; see scenario_mountfs.go's listns arch table).
+const fionread = 0x541B
+
+// ioctlBasic issues a benign, deterministic ioctl on a known fd so the
+// enter_ioctl tracepoint fires under our control rather than only implicitly
+// (via the Go runtime / terminal). ioctl is FamilyFS / KindFd (fd@arg0), so
+// the captured event resolves the fd to the temp file path.
+//
+// We open a regular temp file and call FIONREAD via unix.IoctlGetInt, which
+// reports the number of bytes available to read. On a regular file this is a
+// harmless query that does not mutate state; we ignore its result. The file is
+// cleaned up via the deferred cleanup.
+func ioctlBasic() error {
+ dir, cleanup, err := makeTempDir("ioctl-basic")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "ioctlfile.txt")
+ fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("open: %w", err)
+ }
+ defer syscall.Close(fd)
+
+ // FIONREAD on a regular file is safe and portable. Even if the kernel
+ // rejects it for this fd type, the enter_ioctl tracepoint fires on syscall
+ // entry, so coverage holds regardless of the return value.
+ if _, err := unix.IoctlGetInt(fd, fionread); err != nil {
+ // Tolerated: the sys_enter_ioctl tracepoint has already fired.
+ return nil
+ }
+ return nil
+}
diff --git a/cmd/ioworkload/scenario_mountfs.go b/cmd/ioworkload/scenario_mountfs.go
index 8736da6..5053dca 100644
--- a/cmd/ioworkload/scenario_mountfs.go
+++ b/cmd/ioworkload/scenario_mountfs.go
@@ -100,6 +100,18 @@ func mountfsManagement() error {
_, _, _ = syscall.RawSyscall(unix.SYS_FSMOUNT, ^uintptr(0), 0, 0)
_, _, _ = syscall.RawSyscall(unix.SYS_PIVOT_ROOT, uintptr(unsafe.Pointer(newRoot)), uintptr(unsafe.Pointer(putOld)), 0)
_, _, _ = syscall.RawSyscall6(unix.SYS_QUOTACTL, 0, uintptr(unsafe.Pointer(mountPath)), 0, 0, 0, 0)
+
+ // quotactl_fd(fd, cmd, id, addr) is the fd-based variant of quotactl: it is
+ // a KindFd syscall capturing fd@arg0. We point it at an fd opened on the
+ // mount point directory with best-effort args (Q_GETQUOTA-style cmd, id 0,
+ // nil addr). Quota support / privilege is irrelevant: the sys_enter_
+ // quotactl_fd tracepoint fires on kernel entry before any check, exactly
+ // like the quotactl call above, so MinCount>=1 holds regardless of errno.
+ if quotaFd, err := syscall.Open(mountPoint, syscall.O_RDONLY, 0); err == nil {
+ _, _, _ = syscall.RawSyscall6(unix.SYS_QUOTACTL_FD, uintptr(quotaFd), 0, 0, 0, 0, 0)
+ _ = syscall.Close(quotaFd)
+ }
+
_, _, _ = syscall.RawSyscall(unix.SYS_SWAPON, uintptr(unsafe.Pointer(swapPath)), 0, 0)
_, _, _ = syscall.RawSyscall(unix.SYS_SWAPOFF, uintptr(unsafe.Pointer(swapPath)), 0, 0)
diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go
index cc3123f..22f3e3c 100644
--- a/cmd/ioworkload/scenarios.go
+++ b/cmd/ioworkload/scenarios.go
@@ -74,6 +74,7 @@ var scenarios = map[string]func() error{
"fcntl-dupfd-cloexec": fcntlDupfdCloexec,
"fcntl-invalid-fd": fcntlInvalidFd,
"fcntl-dupfd-max": fcntlDupfdMax,
+ "ioctl-basic": ioctlBasic,
"rename-basic": renameBasic,
"rename-renameat": renameRenameat,
"rename-renameat2": renameRenameat2,
diff --git a/integrationtests/ioctl_test.go b/integrationtests/ioctl_test.go
new file mode 100644
index 0000000..9dbebf7
--- /dev/null
+++ b/integrationtests/ioctl_test.go
@@ -0,0 +1,17 @@
+package integrationtests
+
+import "testing"
+
+// TestIoctlBasic asserts that the ioctl-basic scenario deterministically fires
+// the enter_ioctl tracepoint. ioctl is KindFd (fd@arg0); the fd resolves to the
+// scenario's temp file, so we assert the path as well. Mirrors fcntl_test.go.
+func TestIoctlBasic(t *testing.T) {
+ runScenario(t, "ioctl-basic", []ExpectedEvent{
+ {
+ PathContains: "ioctlfile.txt",
+ Tracepoint: "enter_ioctl",
+ Comm: "ioworkload",
+ MinCount: 1,
+ },
+ })
+}
diff --git a/integrationtests/mountfs_test.go b/integrationtests/mountfs_test.go
index d50706f..69a19fd 100644
--- a/integrationtests/mountfs_test.go
+++ b/integrationtests/mountfs_test.go
@@ -4,7 +4,7 @@ import "testing"
var mountfsTraceArgs = []string{
"-trace-syscalls",
- "mount,umount,move_mount,fsopen,fsconfig,fspick,open_tree,fsmount,pivot_root,quotactl,statmount,listmount,listns,swapon,swapoff",
+ "mount,umount,move_mount,fsopen,fsconfig,fspick,open_tree,fsmount,pivot_root,quotactl,quotactl_fd,statmount,listmount,listns,swapon,swapoff",
}
func TestMountFsManagementSyscalls(t *testing.T) {
@@ -23,6 +23,10 @@ func TestMountFsManagementSyscalls(t *testing.T) {
{Tracepoint: "enter_fsmount", MinCount: 1},
{Tracepoint: "enter_pivot_root", MinCount: 1},
{Tracepoint: "enter_quotactl", MinCount: 1},
+ // quotactl_fd (KindFd, fd@arg0) is the fd-based sibling of quotactl,
+ // issued best-effort on an fd opened on the mount point. Its sys_enter_
+ // tracepoint fires on kernel entry regardless of privilege/quota support.
+ {Tracepoint: "enter_quotactl_fd", MinCount: 1},
{Tracepoint: "enter_statmount", MinCount: 1},
{Tracepoint: "enter_listmount", MinCount: 1},
{Tracepoint: "enter_listns", MinCount: 1},