summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--integrationtests/cmd/ioworkload/scenario_close.go114
-rw-r--r--integrationtests/cmd/ioworkload/scenario_dir.go186
-rw-r--r--integrationtests/cmd/ioworkload/scenario_dup.go151
-rw-r--r--integrationtests/cmd/ioworkload/scenario_fcntl.go129
-rw-r--r--integrationtests/cmd/ioworkload/scenario_iouring.go129
-rw-r--r--integrationtests/cmd/ioworkload/scenario_link.go385
-rw-r--r--integrationtests/cmd/ioworkload/scenario_open.go228
-rw-r--r--integrationtests/cmd/ioworkload/scenario_readwrite.go263
-rw-r--r--integrationtests/cmd/ioworkload/scenario_rename.go251
-rw-r--r--integrationtests/cmd/ioworkload/scenario_stat.go274
-rw-r--r--integrationtests/cmd/ioworkload/scenario_sync.go108
-rw-r--r--integrationtests/cmd/ioworkload/scenario_truncate.go89
-rw-r--r--integrationtests/cmd/ioworkload/scenario_unlink.go185
-rw-r--r--integrationtests/cmd/ioworkload/scenarios.go2386
14 files changed, 2492 insertions, 2386 deletions
diff --git a/integrationtests/cmd/ioworkload/scenario_close.go b/integrationtests/cmd/ioworkload/scenario_close.go
new file mode 100644
index 0000000..a36160a
--- /dev/null
+++ b/integrationtests/cmd/ioworkload/scenario_close.go
@@ -0,0 +1,114 @@
+package main
+
+import (
+ "fmt"
+ "path/filepath"
+ "syscall"
+)
+
+const sysCloseRange = 436
+
+// closeBasic opens multiple files and closes them.
+func closeBasic() error {
+ dir, cleanup, err := makeTempDir("close-basic")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ var fds []int
+ for i := range 3 {
+ path := filepath.Join(dir, fmt.Sprintf("closefile-%d.txt", i))
+ fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("open %d: %w", i, err)
+ }
+ fds = append(fds, fd)
+ }
+ for _, fd := range fds {
+ if err := syscall.Close(fd); err != nil {
+ return fmt.Errorf("close fd %d: %w", fd, err)
+ }
+ }
+ return nil
+}
+
+// closeRange opens multiple files and closes a range of them via close_range(2).
+func closeRange() error {
+ dir, cleanup, err := makeTempDir("close-range")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ var fds []int
+ for i := range 3 {
+ path := filepath.Join(dir, fmt.Sprintf("closerangefile-%d.txt", i))
+ fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("open %d: %w", i, err)
+ }
+ fds = append(fds, fd)
+ }
+
+ if fds[2]-fds[0] != 2 {
+ return fmt.Errorf("fds not contiguous: %v", fds)
+ }
+
+ first := uintptr(fds[0])
+ last := uintptr(fds[len(fds)-1])
+ _, _, errno := syscall.Syscall(sysCloseRange, first, last, 0)
+ if errno != 0 {
+ return fmt.Errorf("close_range: %w", errno)
+ }
+ return nil
+}
+
+// closeInvalidFd attempts to close a very high fd number that is not open.
+// The close fails with EBADF, but ior should capture the enter_close tracepoint
+// because arguments are read on syscall entry before the kernel returns an error.
+func closeInvalidFd() error {
+ err := syscall.Close(99999)
+ if err == nil {
+ return fmt.Errorf("expected close of invalid fd to fail")
+ }
+ return nil
+}
+
+// closeDoubleClose opens a file, closes it normally, then closes the same fd again.
+// The second close fails with EBADF, but ior should capture both enter_close
+// tracepoints because arguments are read on syscall entry.
+func closeDoubleClose() error {
+ dir, cleanup, err := makeTempDir("close-double-close")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "doubleclosefile.txt")
+ fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("open: %w", err)
+ }
+
+ if err := syscall.Close(fd); err != nil {
+ return fmt.Errorf("first close: %w", err)
+ }
+
+ err = syscall.Close(fd)
+ if err == nil {
+ return fmt.Errorf("expected second close of same fd to fail")
+ }
+ return nil
+}
+
+// closeRangeEmpty calls close_range(2) with a range of very high fd numbers
+// (9000–9999) where no fds are open. The syscall succeeds (empty range is valid),
+// and ior should capture the enter_close_range tracepoint.
+func closeRangeEmpty() error {
+ _, _, errno := syscall.Syscall(sysCloseRange, 9000, 9999, 0)
+ if errno != 0 {
+ return fmt.Errorf("close_range: %w", errno)
+ }
+ return nil
+}
diff --git a/integrationtests/cmd/ioworkload/scenario_dir.go b/integrationtests/cmd/ioworkload/scenario_dir.go
new file mode 100644
index 0000000..0c48d54
--- /dev/null
+++ b/integrationtests/cmd/ioworkload/scenario_dir.go
@@ -0,0 +1,186 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+ "syscall"
+ "unsafe"
+)
+
+// dirBasic creates a directory via raw SYS_MKDIR, checks access, then removes it
+// via raw SYS_RMDIR. We use raw syscalls because Go's syscall.Mkdir wraps mkdirat
+// and syscall.Rmdir wraps unlinkat on amd64.
+func dirBasic() error {
+ dir, cleanup, err := makeTempDir("dir-basic")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ subDir := filepath.Join(dir, "subdir")
+ pathBytes, err := syscall.BytePtrFromString(subDir)
+ if err != nil {
+ return fmt.Errorf("path bytes: %w", err)
+ }
+ _, _, errno := syscall.Syscall(syscall.SYS_MKDIR, uintptr(unsafe.Pointer(pathBytes)), 0o755, 0)
+ runtime.KeepAlive(pathBytes)
+ if errno != 0 {
+ return fmt.Errorf("mkdir: %w", errno)
+ }
+
+ if err := syscall.Access(subDir, syscall.F_OK); err != nil {
+ return fmt.Errorf("access: %w", err)
+ }
+
+ pathBytes2, err := syscall.BytePtrFromString(subDir)
+ if err != nil {
+ return fmt.Errorf("path bytes: %w", err)
+ }
+ _, _, errno = syscall.Syscall(syscall.SYS_RMDIR, uintptr(unsafe.Pointer(pathBytes2)), 0, 0)
+ runtime.KeepAlive(pathBytes2)
+ if errno != 0 {
+ return fmt.Errorf("rmdir: %w", errno)
+ }
+ return nil
+}
+
+// dirMkdirat creates a directory via mkdirat(2) using Go's syscall.Mkdir
+// which wraps mkdirat with AT_FDCWD on amd64.
+func dirMkdirat() error {
+ dir, cleanup, err := makeTempDir("dir-mkdirat")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ subDir := filepath.Join(dir, "mkdirat-subdir")
+ if err := syscall.Mkdir(subDir, 0o755); err != nil {
+ return fmt.Errorf("mkdirat: %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 {
+ origDir, err := os.Getwd()
+ if err != nil {
+ return fmt.Errorf("getwd: %w", err)
+ }
+
+ dir, cleanup, err := makeTempDir("dir-chdir")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+ defer syscall.Chdir(origDir)
+
+ if err := syscall.Chdir(dir); err != nil {
+ return fmt.Errorf("chdir: %w", err)
+ }
+ return nil
+}
+
+// dirGetdents opens a directory and reads its entries via getdents64(2).
+func dirGetdents() error {
+ dir, cleanup, err := makeTempDir("dir-getdents")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ // Create a file so getdents has something to return.
+ filePath := filepath.Join(dir, "getdents-file.txt")
+ fd, err := syscall.Open(filePath, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("open file: %w", err)
+ }
+ syscall.Close(fd)
+
+ dirFD, err := syscall.Open(dir, syscall.O_RDONLY|syscall.O_DIRECTORY, 0)
+ if err != nil {
+ return fmt.Errorf("open dir: %w", err)
+ }
+ defer syscall.Close(dirFD)
+
+ buf := make([]byte, 4096)
+ _, _, errno := syscall.Syscall(syscall.SYS_GETDENTS64, uintptr(dirFD), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
+ runtime.KeepAlive(buf)
+ if errno != 0 {
+ return fmt.Errorf("getdents64: %w", errno)
+ }
+ return nil
+}
+
+// dirMkdirEexist attempts to create a directory that already exists via raw
+// SYS_MKDIR. The syscall fails with EEXIST, but ior captures the tracepoint
+// on entry.
+func dirMkdirEexist() error {
+ dir, cleanup, err := makeTempDir("dir-mkdir-eexist")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ subDir := filepath.Join(dir, "mkdir-eexist-subdir")
+ pathBytes, err := syscall.BytePtrFromString(subDir)
+ if err != nil {
+ return fmt.Errorf("path bytes: %w", err)
+ }
+
+ // Create the directory first so the second attempt fails.
+ _, _, errno := syscall.Syscall(syscall.SYS_MKDIR, uintptr(unsafe.Pointer(pathBytes)), 0o755, 0)
+ runtime.KeepAlive(pathBytes)
+ if errno != 0 {
+ return fmt.Errorf("first mkdir: %w", errno)
+ }
+
+ // Second mkdir on the same path should fail with EEXIST.
+ pathBytes2, err := syscall.BytePtrFromString(subDir)
+ if err != nil {
+ return fmt.Errorf("path bytes: %w", err)
+ }
+ _, _, errno = syscall.Syscall(syscall.SYS_MKDIR, uintptr(unsafe.Pointer(pathBytes2)), 0o755, 0)
+ runtime.KeepAlive(pathBytes2)
+ if errno == 0 {
+ return fmt.Errorf("expected EEXIST, but mkdir succeeded")
+ }
+ return nil
+}
+
+// dirChdirEnoent attempts to change to a nonexistent directory via raw
+// SYS_CHDIR. The syscall fails with ENOENT, but ior captures the tracepoint
+// on entry.
+func dirChdirEnoent() error {
+ dir, cleanup, err := makeTempDir("dir-chdir-enoent")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ badPath := filepath.Join(dir, "chdir-enoent-missing")
+ pathBytes, err := syscall.BytePtrFromString(badPath)
+ if err != nil {
+ return fmt.Errorf("path bytes: %w", err)
+ }
+ _, _, errno := syscall.Syscall(syscall.SYS_CHDIR, uintptr(unsafe.Pointer(pathBytes)), 0, 0)
+ runtime.KeepAlive(pathBytes)
+ if errno == 0 {
+ return fmt.Errorf("expected ENOENT, but chdir succeeded")
+ }
+ return nil
+}
+
+// dirGetdentsEbadf calls getdents64(2) with an invalid file descriptor.
+// The syscall fails with EBADF, but ior captures the tracepoint on entry.
+func dirGetdentsEbadf() error {
+ buf := make([]byte, 4096)
+ _, _, errno := syscall.Syscall(syscall.SYS_GETDENTS64, uintptr(9999), uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
+ runtime.KeepAlive(buf)
+ if errno == 0 {
+ return fmt.Errorf("expected EBADF, but getdents64 succeeded")
+ }
+ return nil
+}
diff --git a/integrationtests/cmd/ioworkload/scenario_dup.go b/integrationtests/cmd/ioworkload/scenario_dup.go
new file mode 100644
index 0000000..6a89970
--- /dev/null
+++ b/integrationtests/cmd/ioworkload/scenario_dup.go
@@ -0,0 +1,151 @@
+package main
+
+import (
+ "fmt"
+ "path/filepath"
+ "syscall"
+)
+
+// dupBasic opens a file, dups the fd, writes via the dup, closes both.
+func dupBasic() error {
+ dir, cleanup, err := makeTempDir("dup-basic")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "dupfile.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)
+
+ newFd, err := syscall.Dup(fd)
+ if err != nil {
+ return fmt.Errorf("dup: %w", err)
+ }
+ defer syscall.Close(newFd)
+
+ if _, err := syscall.Write(newFd, []byte("via dup")); err != nil {
+ return fmt.Errorf("write via dup: %w", err)
+ }
+ return nil
+}
+
+// dupDup2 opens a file and duplicates the fd onto a specific target fd via dup2.
+func dupDup2() error {
+ dir, cleanup, err := makeTempDir("dup-dup2")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "dup2file.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)
+
+ // Use a high fd number to avoid collisions.
+ targetFd := 500
+ if err := syscall.Dup2(fd, targetFd); err != nil {
+ return fmt.Errorf("dup2: %w", err)
+ }
+ defer syscall.Close(targetFd)
+
+ if _, err := syscall.Write(targetFd, []byte("via dup2")); err != nil {
+ return fmt.Errorf("write via dup2: %w", err)
+ }
+ return nil
+}
+
+// dupDup3 opens a file and duplicates the fd onto a specific target fd via dup3
+// with O_CLOEXEC flag.
+func dupDup3() error {
+ dir, cleanup, err := makeTempDir("dup-dup3")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "dup3file.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)
+
+ // Use a high fd number to avoid collisions.
+ targetFd := 501
+ if err := syscall.Dup3(fd, targetFd, syscall.O_CLOEXEC); err != nil {
+ return fmt.Errorf("dup3: %w", err)
+ }
+ defer syscall.Close(targetFd)
+
+ if _, err := syscall.Write(targetFd, []byte("via dup3")); err != nil {
+ return fmt.Errorf("write via dup3: %w", err)
+ }
+ return nil
+}
+
+// dupInvalidFd attempts to dup a very high invalid fd number.
+// The syscall fails with EBADF, but ior should capture the enter_dup
+// tracepoint because arguments are read on syscall entry.
+func dupInvalidFd() error {
+ _, err := syscall.Dup(99999)
+ if err == nil {
+ return fmt.Errorf("expected dup of invalid fd to fail")
+ }
+ return nil
+}
+
+// dup2SameFd calls dup2 with the same fd for both oldfd and newfd.
+// Per POSIX, dup2(fd, fd) is a no-op that returns fd without closing
+// and reopening. ior should capture the enter_dup2 tracepoint.
+func dup2SameFd() error {
+ dir, cleanup, err := makeTempDir("dup2-same-fd")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "dup2samefile.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)
+
+ if err := syscall.Dup2(fd, fd); err != nil {
+ return fmt.Errorf("dup2 same fd: %w", err)
+ }
+ return nil
+}
+
+// dup3InvalidFlags calls dup3 with an invalid flags value.
+// dup3 only accepts O_CLOEXEC; any other flag causes EINVAL.
+// ior should capture the enter_dup3 tracepoint.
+func dup3InvalidFlags() error {
+ dir, cleanup, err := makeTempDir("dup3-invalid-flags")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "dup3flagsfile.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)
+
+ targetFd := 502
+ _, _, errno := syscall.Syscall(syscall.SYS_DUP3, uintptr(fd), uintptr(targetFd), 0xBAD)
+ if errno == 0 {
+ syscall.Close(targetFd)
+ return fmt.Errorf("expected dup3 with invalid flags to fail")
+ }
+ return nil
+}
diff --git a/integrationtests/cmd/ioworkload/scenario_fcntl.go b/integrationtests/cmd/ioworkload/scenario_fcntl.go
new file mode 100644
index 0000000..0d8e642
--- /dev/null
+++ b/integrationtests/cmd/ioworkload/scenario_fcntl.go
@@ -0,0 +1,129 @@
+package main
+
+import (
+ "fmt"
+ "path/filepath"
+ "syscall"
+)
+
+// fcntlDupfd uses fcntl F_DUPFD to duplicate a file descriptor.
+func fcntlDupfd() error {
+ dir, cleanup, err := makeTempDir("fcntl-dupfd")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "fcntlfile.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)
+
+ newFd, _, errno := syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), syscall.F_DUPFD, 0)
+ if errno != 0 {
+ return fmt.Errorf("fcntl F_DUPFD: %w", errno)
+ }
+ defer syscall.Close(int(newFd))
+
+ if _, err := syscall.Write(int(newFd), []byte("via fcntl")); err != nil {
+ return fmt.Errorf("write via fcntl dup: %w", err)
+ }
+ return nil
+}
+
+// fcntlSetfl uses fcntl F_GETFL/F_SETFL to read and modify file status flags.
+func fcntlSetfl() error {
+ dir, cleanup, err := makeTempDir("fcntl-setfl")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "fcntlsetflfile.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)
+
+ flags, _, errno := syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), syscall.F_GETFL, 0)
+ if errno != 0 {
+ return fmt.Errorf("fcntl F_GETFL: %w", errno)
+ }
+
+ _, _, errno = syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), syscall.F_SETFL, flags|syscall.O_APPEND)
+ if errno != 0 {
+ return fmt.Errorf("fcntl F_SETFL: %w", errno)
+ }
+
+ if _, err := syscall.Write(fd, []byte("appended via fcntl setfl")); err != nil {
+ return fmt.Errorf("write: %w", err)
+ }
+ return nil
+}
+
+// fcntlDupfdCloexec uses fcntl F_DUPFD_CLOEXEC to duplicate a file descriptor
+// with the close-on-exec flag set.
+func fcntlDupfdCloexec() error {
+ dir, cleanup, err := makeTempDir("fcntl-dupfd-cloexec")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "fcntlcloexecfile.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)
+
+ newFd, _, errno := syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), syscall.F_DUPFD_CLOEXEC, 0)
+ if errno != 0 {
+ return fmt.Errorf("fcntl F_DUPFD_CLOEXEC: %w", errno)
+ }
+ defer syscall.Close(int(newFd))
+
+ if _, err := syscall.Write(int(newFd), []byte("via fcntl dupfd cloexec")); err != nil {
+ return fmt.Errorf("write via fcntl dup cloexec: %w", err)
+ }
+ return nil
+}
+
+// fcntlInvalidFd calls fcntl F_GETFL on an invalid fd (99999).
+// The syscall fails with EBADF, but ior should capture the enter_fcntl
+// tracepoint because it is recorded on syscall entry.
+func fcntlInvalidFd() error {
+ _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, 99999, syscall.F_GETFL, 0)
+ if errno == 0 {
+ return fmt.Errorf("expected fcntl on invalid fd to fail")
+ }
+ return nil
+}
+
+// fcntlDupfdMax opens a file and calls fcntl F_DUPFD with a minfd value
+// that exceeds the process RLIMIT_NOFILE. The kernel rejects this with
+// EINVAL, but ior should capture the enter_fcntl tracepoint.
+func fcntlDupfdMax() error {
+ dir, cleanup, err := makeTempDir("fcntl-dupfd-max")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ path := filepath.Join(dir, "fcntldupfdmaxfile.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)
+
+ // Use a minfd far beyond any realistic RLIMIT_NOFILE.
+ _, _, errno := syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), syscall.F_DUPFD, 1<<30)
+ if errno == 0 {
+ return fmt.Errorf("expected fcntl F_DUPFD with extreme minfd to fail")
+ }
+ return nil
+}
diff --git a/integrationtests/cmd/ioworkload/scenario_iouring.go b/integrationtests/cmd/ioworkload/scenario_iouring.go
new file mode 100644
index 0000000..b1aac4e
--- /dev/null
+++ b/integrationtests/cmd/ioworkload/scenario_iouring.go
@@ -0,0 +1,129 @@
+package main
+
+import (
+ "fmt"
+ "runtime"
+ "syscall"
+ "unsafe"
+)
+
+const (
+ sysIoUringSetup = 425
+ sysIoUringEnter = 426
+ sysIoUringRegister = 427
+
+ // io_uring_params struct size: 10 x uint32 + io_sqring_offsets(40) + io_cqring_offsets(40) = 120 bytes.
+ ioUringParamsSize = 120
+
+ ioringRegisterProbe = 8 // IORING_REGISTER_PROBE
+)
+
+// iouringSetup creates an io_uring instance via io_uring_setup(2) and closes the fd.
+func iouringSetup() error {
+ fd, err := ioUringSetupRing(1)
+ if err != nil {
+ return err
+ }
+ return syscall.Close(fd)
+}
+
+// iouringEnter creates an io_uring instance, then calls io_uring_enter(2)
+// with zero submissions/completions to exercise the enter tracepoint.
+func iouringEnter() error {
+ fd, err := ioUringSetupRing(1)
+ if err != nil {
+ return err
+ }
+ defer syscall.Close(fd)
+
+ _, _, errno := syscall.Syscall6(
+ sysIoUringEnter,
+ uintptr(fd),
+ 0, // to_submit
+ 0, // min_complete
+ 0, // flags
+ 0, // sig
+ 0, // sz
+ )
+ if errno != 0 {
+ return fmt.Errorf("io_uring_enter: %w", errno)
+ }
+ return nil
+}
+
+// iouringRegister creates an io_uring instance, then calls io_uring_register(2)
+// with IORING_REGISTER_PROBE to exercise the register tracepoint.
+func iouringRegister() error {
+ fd, err := ioUringSetupRing(1)
+ if err != nil {
+ return err
+ }
+ defer syscall.Close(fd)
+
+ // io_uring_probe header is 16 bytes; we don't need probe_op entries.
+ var probeBuf [16]byte
+ _, _, errno := syscall.Syscall6(
+ sysIoUringRegister,
+ uintptr(fd),
+ ioringRegisterProbe,
+ uintptr(unsafe.Pointer(&probeBuf[0])),
+ 0, // nr_args (0 ops requested)
+ 0, 0,
+ )
+ runtime.KeepAlive(probeBuf)
+ if errno != 0 {
+ return fmt.Errorf("io_uring_register: %w", errno)
+ }
+ return nil
+}
+
+// iouringEnterEbadf calls io_uring_enter on an invalid fd.
+// The syscall fails with EBADF, but ior captures the enter_io_uring_enter tracepoint.
+func iouringEnterEbadf() error {
+ _, _, errno := syscall.Syscall6(
+ sysIoUringEnter,
+ 99999, // invalid fd
+ 0, // to_submit
+ 0, // min_complete
+ 0, // flags
+ 0, // sig
+ 0, // sz
+ )
+ if errno == 0 {
+ return fmt.Errorf("expected EBADF, but io_uring_enter succeeded")
+ }
+ return nil
+}
+
+// iouringRegisterEbadf calls io_uring_register on an invalid fd.
+// The syscall fails with EBADF, but ior captures the enter_io_uring_register tracepoint.
+func iouringRegisterEbadf() error {
+ _, _, errno := syscall.Syscall6(
+ sysIoUringRegister,
+ 99999, // invalid fd
+ ioringRegisterProbe,
+ 0, // arg (NULL)
+ 0, // nr_args
+ 0, 0,
+ )
+ if errno == 0 {
+ return fmt.Errorf("expected EBADF, but io_uring_register succeeded")
+ }
+ return nil
+}
+
+// ioUringSetupRing calls io_uring_setup(2) and returns the ring fd.
+func ioUringSetupRing(entries uint32) (int, error) {
+ var params [ioUringParamsSize]byte
+ fd, _, errno := syscall.Syscall(
+ sysIoUringSetup,
+ uintptr(entries),
+ uintptr(unsafe.Pointer(&params[0])),
+ 0,
+ )
+ runtime.KeepAlive(params)
+ if errno != 0 {
+ return 0, fmt.Errorf("io_uring_setup: %w", errno)
+ }
+ return int(fd), nil
+}
diff --git a/integrationtests/cmd/ioworkload/scenario_link.go b/integrationtests/cmd/ioworkload/scenario_link.go
new file mode 100644
index 0000000..bb16984
--- /dev/null
+++ b/integrationtests/cmd/ioworkload/scenario_link.go
@@ -0,0 +1,385 @@
+package main
+
+import (
+ "fmt"
+ "path/filepath"
+ "runtime"
+ "syscall"
+ "unsafe"
+)
+
+// linkBasic creates a file, hard links it via link(2), symlinks it via
+// symlink(2), and reads the symlink via readlink(2).
+// Uses raw SYS_LINK, SYS_SYMLINK, SYS_READLINK because Go's syscall wrappers
+// delegate to linkat/symlinkat/readlinkat on amd64.
+func linkBasic() error {
+ dir, cleanup, err := makeTempDir("link-basic")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ origPath := filepath.Join(dir, "original.txt")
+ fd, err := syscall.Open(origPath, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("open: %w", err)
+ }
+ if err := syscall.Close(fd); err != nil {
+ return fmt.Errorf("close: %w", err)
+ }
+
+ if err := rawLink(origPath, filepath.Join(dir, "hardlink.txt")); err != nil {
+ return err
+ }
+
+ symPath := filepath.Join(dir, "symlink.txt")
+ if err := rawSymlink(origPath, symPath); err != nil {
+ return err
+ }
+
+ return rawReadlink(symPath)
+}
+
+// linkLinkat creates a file and hard links it via linkat(2).
+func linkLinkat() error {
+ dir, cleanup, err := makeTempDir("link-linkat")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ origName := "linkat-original.txt"
+ origPath := filepath.Join(dir, origName)
+ fd, err := syscall.Open(origPath, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("open: %w", err)
+ }
+ if err := syscall.Close(fd); err != nil {
+ return fmt.Errorf("close: %w", err)
+ }
+
+ dirFD, err := syscall.Open(dir, syscall.O_RDONLY|syscall.O_DIRECTORY, 0)
+ if err != nil {
+ return fmt.Errorf("open dir: %w", err)
+ }
+ defer syscall.Close(dirFD)
+
+ hardName := "linkat-hard.txt"
+ oldBytes, err := syscall.BytePtrFromString(origName)
+ if err != nil {
+ return fmt.Errorf("old name bytes: %w", err)
+ }
+ newBytes, err := syscall.BytePtrFromString(hardName)
+ if err != nil {
+ return fmt.Errorf("new name bytes: %w", err)
+ }
+
+ _, _, errno := syscall.Syscall6(
+ syscall.SYS_LINKAT,
+ uintptr(dirFD),
+ uintptr(unsafe.Pointer(oldBytes)),
+ uintptr(dirFD),
+ uintptr(unsafe.Pointer(newBytes)),
+ 0, // flags
+ 0,
+ )
+ runtime.KeepAlive(oldBytes)
+ runtime.KeepAlive(newBytes)
+ if errno != 0 {
+ return fmt.Errorf("linkat: %w", errno)
+ }
+ return nil
+}
+
+// linkSymlinkat creates a symlink via symlinkat(2).
+func linkSymlinkat() error {
+ dir, cleanup, err := makeTempDir("link-symlinkat")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ origPath := filepath.Join(dir, "symlinkat-original.txt")
+ fd, err := syscall.Open(origPath, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("open: %w", err)
+ }
+ if err := syscall.Close(fd); err != nil {
+ return fmt.Errorf("close: %w", err)
+ }
+
+ dirFD, err := syscall.Open(dir, syscall.O_RDONLY|syscall.O_DIRECTORY, 0)
+ if err != nil {
+ return fmt.Errorf("open dir: %w", err)
+ }
+ defer syscall.Close(dirFD)
+
+ targetBytes, err := syscall.BytePtrFromString(origPath)
+ if err != nil {
+ return fmt.Errorf("target bytes: %w", err)
+ }
+ linkName := "symlinkat-link.txt"
+ linkBytes, err := syscall.BytePtrFromString(linkName)
+ if err != nil {
+ return fmt.Errorf("link name bytes: %w", err)
+ }
+
+ _, _, errno := syscall.Syscall(
+ syscall.SYS_SYMLINKAT,
+ uintptr(unsafe.Pointer(targetBytes)),
+ uintptr(dirFD),
+ uintptr(unsafe.Pointer(linkBytes)),
+ )
+ runtime.KeepAlive(targetBytes)
+ runtime.KeepAlive(linkBytes)
+ if errno != 0 {
+ return fmt.Errorf("symlinkat: %w", errno)
+ }
+ return nil
+}
+
+// linkReadlinkat creates a symlink, then reads it via readlinkat(2).
+func linkReadlinkat() error {
+ dir, cleanup, err := makeTempDir("link-readlinkat")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ origPath := filepath.Join(dir, "readlinkat-original.txt")
+ fd, err := syscall.Open(origPath, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("open: %w", err)
+ }
+ if err := syscall.Close(fd); err != nil {
+ return fmt.Errorf("close: %w", err)
+ }
+
+ // Create symlink using raw SYS_SYMLINK so we don't mix tracepoints.
+ linkPath := filepath.Join(dir, "readlinkat-link.txt")
+ if err := rawSymlink(origPath, linkPath); err != nil {
+ return err
+ }
+
+ // Read via readlinkat(2).
+ dirFD, err := syscall.Open(dir, syscall.O_RDONLY|syscall.O_DIRECTORY, 0)
+ if err != nil {
+ return fmt.Errorf("open dir: %w", err)
+ }
+ defer syscall.Close(dirFD)
+
+ linkName := "readlinkat-link.txt"
+ nameBytes, err := syscall.BytePtrFromString(linkName)
+ if err != nil {
+ return fmt.Errorf("link name bytes: %w", err)
+ }
+ buf := make([]byte, 256)
+ _, _, errno := syscall.Syscall6(
+ syscall.SYS_READLINKAT,
+ uintptr(dirFD),
+ uintptr(unsafe.Pointer(nameBytes)),
+ uintptr(unsafe.Pointer(&buf[0])),
+ uintptr(len(buf)),
+ 0, 0,
+ )
+ runtime.KeepAlive(nameBytes)
+ runtime.KeepAlive(buf)
+ if errno != 0 {
+ return fmt.Errorf("readlinkat: %w", errno)
+ }
+ return nil
+}
+
+// linkEnoent attempts to hard link a nonexistent source via raw SYS_LINK.
+// The syscall fails with ENOENT, but ior captures the enter_link tracepoint
+// because arguments are read on syscall entry.
+func linkEnoent() error {
+ dir, cleanup, err := makeTempDir("link-enoent")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ srcPath := filepath.Join(dir, "link-enoent-missing.txt")
+ dstPath := filepath.Join(dir, "link-enoent-dst.txt")
+
+ srcBytes, err := syscall.BytePtrFromString(srcPath)
+ if err != nil {
+ return fmt.Errorf("src path bytes: %w", err)
+ }
+ dstBytes, err := syscall.BytePtrFromString(dstPath)
+ if err != nil {
+ return fmt.Errorf("dst path bytes: %w", err)
+ }
+
+ _, _, errno := syscall.Syscall(
+ syscall.SYS_LINK,
+ uintptr(unsafe.Pointer(srcBytes)),
+ uintptr(unsafe.Pointer(dstBytes)),
+ 0,
+ )
+ runtime.KeepAlive(srcBytes)
+ runtime.KeepAlive(dstBytes)
+ if errno == 0 {
+ return fmt.Errorf("expected ENOENT, but link succeeded")
+ }
+ return nil
+}
+
+// linkSymlinkEexist creates a regular file, then attempts to create a symlink
+// at the same path via raw SYS_SYMLINK. The syscall fails with EEXIST because
+// the link path already exists, but ior captures the enter_symlink tracepoint.
+func linkSymlinkEexist() error {
+ dir, cleanup, err := makeTempDir("link-symlink-eexist")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ existingPath := filepath.Join(dir, "symlink-eexist.txt")
+ fd, err := syscall.Open(existingPath, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("open: %w", err)
+ }
+ if err := syscall.Close(fd); err != nil {
+ return fmt.Errorf("close: %w", err)
+ }
+
+ targetBytes, err := syscall.BytePtrFromString("/tmp/dummy-target")
+ if err != nil {
+ return fmt.Errorf("target bytes: %w", err)
+ }
+ linkBytes, err := syscall.BytePtrFromString(existingPath)
+ if err != nil {
+ return fmt.Errorf("link path bytes: %w", err)
+ }
+
+ _, _, errno := syscall.Syscall(
+ syscall.SYS_SYMLINK,
+ uintptr(unsafe.Pointer(targetBytes)),
+ uintptr(unsafe.Pointer(linkBytes)),
+ 0,
+ )
+ runtime.KeepAlive(targetBytes)
+ runtime.KeepAlive(linkBytes)
+ if errno == 0 {
+ return fmt.Errorf("expected EEXIST, but symlink succeeded")
+ }
+ return nil
+}
+
+// linkReadlinkatEinval creates a regular file and calls readlinkat(2) on it.
+// The syscall fails with EINVAL because the path is not a symlink, but ior
+// captures the enter_readlinkat tracepoint on syscall entry.
+func linkReadlinkatEinval() error {
+ dir, cleanup, err := makeTempDir("link-readlinkat-einval")
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+
+ regularFile := "readlinkat-einval.txt"
+ regularPath := filepath.Join(dir, regularFile)
+ fd, err := syscall.Open(regularPath, syscall.O_RDWR|syscall.O_CREAT, 0o644)
+ if err != nil {
+ return fmt.Errorf("open: %w", err)
+ }
+ if err := syscall.Close(fd); err != nil {
+ return fmt.Errorf("close: %w", err)
+ }
+
+ dirFD, err := syscall.Open(dir, syscall.O_RDONLY|syscall.O_DIRECTORY, 0)
+ if err != nil {
+ return fmt.Errorf("open dir: %w", err)
+ }
+ defer syscall.Close(dirFD)
+
+ nameBytes, err := syscall.BytePtrFromString(regularFile)
+ if err != nil {
+ return fmt.Errorf("name bytes: %w", err)
+ }
+ buf := make([]byte, 256)
+ _, _, errno := syscall.Syscall6(
+ syscall.SYS_READLINKAT,
+ uintptr(dirFD),
+ uintptr(unsafe.Pointer(nameBytes)),
+ uintptr(unsafe.Pointer(&buf[0])),
+ uintptr(len(buf)),
+ 0, 0,
+ )
+ runtime.KeepAlive(nameBytes)
+ runtime.KeepAlive(buf)
+ if errno == 0 {
+ return fmt.Errorf("expected EINVAL, but readlinkat succeeded")
+ }
+ return nil
+}
+
+// rawLink calls link(2) via raw SYS_LINK.
+func rawLink(oldPath, newPath string) error {
+ oldBytes, err := syscall.BytePtrFromString(oldPath)
+ if err != nil {
+ return fmt.Errorf("old path bytes: %w", err)
+ }
+ newBytes, err := syscall.BytePtrFromString(newPath)
+ if err != nil {
+ return fmt.Errorf("new path bytes: %w", err)
+ }
+ _, _, errno := syscall.Syscall(
+ syscall.SYS_LINK,
+ uintptr(unsafe.Pointer(oldBytes)),
+ uintptr(unsafe.Pointer(newBytes)),
+ 0,
+ )
+ runtime.KeepAlive(oldBytes)
+ runtime.KeepAlive(newBytes)
+ if errno != 0 {
+ return fmt.Errorf("link: %w", errno)
+ }
+ return nil
+}
+
+// rawSymlink calls symlink(2) via raw SYS_SYMLINK.
+func rawSymlink(target, linkPath string) error {
+ targetBytes, err := syscall.BytePtrFromString(target)
+ if err != nil {
+ return fmt.Errorf("target path bytes: %w", err)
+ }
+ linkBytes, err := syscall.BytePtrFromString(linkPath)
+ if err != nil {