diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-24 20:36:26 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-24 20:36:26 +0300 |
| commit | 92a36a8c5f23756b8c6d721e89450752409ddd75 (patch) | |
| tree | 52adee49828831feb0ca557e7df736726faedac3 /cmd | |
| parent | fadbf135d0b251387fd785083df79e27d1025cac (diff) | |
task a8: move all binaries under ./cmd/<name>/main.go
Relocates the two non-canonical main packages so every binary in the repo
lives at ./cmd/<BINARY>/main.go:
- tools/filewriter/ -> cmd/filewriter/
- integrationtests/cmd/ioworkload/ (20 files) -> cmd/ioworkload/
Consumers updated:
- Magefile.go: workloadSourcePath now ./cmd/ioworkload
- integrationtests/README.md: structure note points at ../cmd/ioworkload
Files moved with git mv so git log --follow history is preserved.
cmd/ior/main.go was already canonical and is untouched.
Verified: mage build produces the ior binary; go build ./cmd/...
builds filewriter and ioworkload; go test ./cmd/ioworkload passes;
go vet ./cmd/filewriter ./cmd/ioworkload is clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/filewriter/main.go | 35 | ||||
| -rw-r--r-- | cmd/ioworkload/main.go | 49 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_close.go | 117 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_copy_file_range.go | 81 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_dir.go | 224 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_dup.go | 151 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_fcntl.go | 134 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_iouring.go | 133 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_link.go | 391 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_mmap.go | 110 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_open.go | 270 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_pidfd.go | 133 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_pidfd_test.go | 57 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_readwrite.go | 263 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_rename.go | 253 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_stat.go | 286 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_sync.go | 137 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_truncate.go | 93 | ||||
| -rw-r--r-- | cmd/ioworkload/scenario_unlink.go | 193 | ||||
| -rw-r--r-- | cmd/ioworkload/scenarios.go | 118 |
20 files changed, 3228 insertions, 0 deletions
diff --git a/cmd/filewriter/main.go b/cmd/filewriter/main.go new file mode 100644 index 0000000..25f5cb7 --- /dev/null +++ b/cmd/filewriter/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "os" + "time" +) + +func main() { + // Open the file in append mode, create it if it doesn't exist + file, err := os.OpenFile("output.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + panic(err) + } + defer file.Close() + + // Define the byte to be written + data := []byte("A") // Replace 'A' with any byte you wish to write + + // Loop to write the byte every 3 seconds + for { + _, err := file.Write(data) + if err != nil { + panic(err) + } + + // Flush writes to stable storage + err = file.Sync() + if err != nil { + panic(err) + } + + // Wait for 3 seconds + time.Sleep(3 * time.Second) + } +} diff --git a/cmd/ioworkload/main.go b/cmd/ioworkload/main.go new file mode 100644 index 0000000..0276a9c --- /dev/null +++ b/cmd/ioworkload/main.go @@ -0,0 +1,49 @@ +// ioworkload is a standalone binary that performs deterministic I/O operations +// for integration testing of ior. It prints its PID to stdout, sleeps to allow +// ior to attach BPF tracepoints, then executes the requested I/O scenario. +package main + +import ( + "flag" + "fmt" + "os" + "slices" + "time" +) + +// Give ior enough time to attach tracepoints before scenarios emit syscalls. +// Under slower CI or locally saturated systems, 5s can still miss first-call +// events for single-shot scenarios. Use a slightly larger delay for stability. +const startupDelay = 8 * time.Second + +func main() { + scenario := flag.String("scenario", "", "I/O scenario to execute") + flag.Parse() + + if *scenario == "" { + fmt.Fprintln(os.Stderr, "usage: ioworkload --scenario=<name>") + os.Exit(2) + } + + run, ok := scenarios[*scenario] + if !ok { + fmt.Fprintf(os.Stderr, "unknown scenario: %s\navailable scenarios:\n", *scenario) + var names []string + for name := range scenarios { + names = append(names, name) + } + slices.Sort(names) + for _, name := range names { + fmt.Fprintf(os.Stderr, " %s\n", name) + } + os.Exit(2) + } + + fmt.Println(os.Getpid()) + time.Sleep(startupDelay) + + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "scenario %s failed: %v\n", *scenario, err) + os.Exit(1) + } +} diff --git a/cmd/ioworkload/scenario_close.go b/cmd/ioworkload/scenario_close.go new file mode 100644 index 0000000..fc5044c --- /dev/null +++ b/cmd/ioworkload/scenario_close.go @@ -0,0 +1,117 @@ +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 { + // Retry a few times to reduce event-loss flakiness under heavy test load. + for i := 0; i < 5; i++ { + _, _, errno := syscall.Syscall(sysCloseRange, 9000, 9999, 0) + if errno != 0 { + return fmt.Errorf("close_range: %w", errno) + } + } + return nil +} diff --git a/cmd/ioworkload/scenario_copy_file_range.go b/cmd/ioworkload/scenario_copy_file_range.go new file mode 100644 index 0000000..ce0524e --- /dev/null +++ b/cmd/ioworkload/scenario_copy_file_range.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + "path/filepath" + "syscall" +) + +// SYS_COPY_FILE_RANGE on x86_64 Linux. +const sysCopyFileRange = 326 + +// copyFileRangeBasic copies bytes from a source file to a destination file +// using copy_file_range(2) with flags=0 as required by the manpage. +func copyFileRangeBasic() error { + dir, cleanup, err := makeTempDir("copy-file-range-basic") + if err != nil { + return err + } + defer cleanup() + + srcPath := filepath.Join(dir, "copyrangesrc.txt") + dstPath := filepath.Join(dir, "copyrangedst.txt") + + srcFd, err := syscall.Open(srcPath, syscall.O_RDWR|syscall.O_CREAT|syscall.O_TRUNC, 0o644) + if err != nil { + return fmt.Errorf("open source: %w", err) + } + defer syscall.Close(srcFd) + + dstFd, err := syscall.Open(dstPath, syscall.O_RDWR|syscall.O_CREAT|syscall.O_TRUNC, 0o644) + if err != nil { + return fmt.Errorf("open destination: %w", err) + } + defer syscall.Close(dstFd) + + data := []byte("copy_file_range integration data") + if _, err := syscall.Write(srcFd, data); err != nil { + return fmt.Errorf("write source: %w", err) + } + if _, err := syscall.Seek(srcFd, 0, 0); err != nil { + return fmt.Errorf("seek source: %w", err) + } + + n, _, errno := syscall.Syscall6(uintptr(sysCopyFileRange), uintptr(srcFd), 0, uintptr(dstFd), 0, uintptr(len(data)), 0) + if errno != 0 { + return fmt.Errorf("copy_file_range: %w", errno) + } + if n == 0 { + return fmt.Errorf("copy_file_range copied 0 bytes") + } + + return nil +} + +// copyFileRangeBadDstFd calls copy_file_range(2) with an invalid destination fd. +// The syscall should fail with EBADF, while still emitting the enter tracepoint. +func copyFileRangeBadDstFd() error { + dir, cleanup, err := makeTempDir("copy-file-range-bad-dst") + if err != nil { + return err + } + defer cleanup() + + srcPath := filepath.Join(dir, "copyrangeebadfsrc.txt") + srcFd, err := syscall.Open(srcPath, syscall.O_RDWR|syscall.O_CREAT|syscall.O_TRUNC, 0o644) + if err != nil { + return fmt.Errorf("open source: %w", err) + } + defer syscall.Close(srcFd) + + if _, err := syscall.Write(srcFd, []byte("copy_file_range ebadf data")); err != nil { + return fmt.Errorf("write source: %w", err) + } + + _, _, errno := syscall.Syscall6(uintptr(sysCopyFileRange), uintptr(srcFd), 0, uintptr(99999), 0, uintptr(16), 0) + if errno != syscall.EBADF { + return fmt.Errorf("expected EBADF from copy_file_range with invalid dst fd, got %v", errno) + } + + return nil +} diff --git a/cmd/ioworkload/scenario_dir.go b/cmd/ioworkload/scenario_dir.go new file mode 100644 index 0000000..7a78716 --- /dev/null +++ b/cmd/ioworkload/scenario_dir.go @@ -0,0 +1,224 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "syscall" + "time" + "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 +} + +// dirGetcwd changes into a temp directory and calls getcwd(2) directly. +func dirGetcwd() error { + origDir, err := os.Getwd() + if err != nil { + return fmt.Errorf("getwd: %w", err) + } + + dir, cleanup, err := makeTempDir("dir-getcwd") + if err != nil { + return err + } + defer cleanup() + defer syscall.Chdir(origDir) + + if err := syscall.Chdir(dir); err != nil { + return fmt.Errorf("chdir: %w", err) + } + + buf := make([]byte, 4096) + _, _, errno := syscall.Syscall(syscall.SYS_GETCWD, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)), 0) + runtime.KeepAlive(buf) + if errno != 0 { + return fmt.Errorf("getcwd: %w", errno) + } + // Keep cwd unchanged long enough for ior to process enter/exit pairing. + time.Sleep(300 * time.Millisecond) + 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) + } + // Retry a few times to reduce dropped-event flakiness under high load. + for i := 0; i < 5; i++ { + _, _, 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) + // Keep issuing the syscall for a short window so ior has enough time to + // attach under high parallel integration load. + for i := 0; i < 40; i++ { + _, _, 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") + } + time.Sleep(25 * time.Millisecond) + } + return nil +} diff --git a/cmd/ioworkload/scenario_dup.go b/cmd/ioworkload/scenario_dup.go new file mode 100644 index 0000000..6a89970 --- /dev/null +++ b/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/cmd/ioworkload/scenario_fcntl.go b/cmd/ioworkload/scenario_fcntl.go new file mode 100644 index 0000000..0c97002 --- /dev/null +++ b/cmd/ioworkload/scenario_fcntl.go @@ -0,0 +1,134 @@ +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 { + for i := 0; i < 5; i++ { + _, _, 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) + + // Retry the failing fcntl a few times to avoid a single one-shot call + // racing early trace capture under parallel integration load. + for i := 0; i < 5; i++ { + _, _, 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/cmd/ioworkload/scenario_iouring.go b/cmd/ioworkload/scenario_iouring.go new file mode 100644 index 0000000..a16d59a --- /dev/null +++ b/cmd/ioworkload/scenario_iouring.go @@ -0,0 +1,133 @@ +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 { + for i := 0; i < 5; i++ { + _, _, 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 { + for i := 0; i < 5; i++ { + _, _, 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(¶ms[0])), + 0, + ) + runtime.KeepAlive(params) + if errno != 0 { + return 0, fmt.Errorf("io_uring_setup: %w", errno) + } + return int(fd), nil +} diff --git a/cmd/ioworkload/scenario_link.go b/cmd/ioworkload/scenario_link.go new file mode 100644 index 0000000..beb49a0 --- /dev/null +++ b/cmd/ioworkload/scenario_link.go @@ -0,0 +1,391 @@ +package main + +import ( + "fmt" + "path/filepath" + "runtime" + "syscall" + "time" + "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) + } + + // Issue the same failing syscall a few times to make capture robust even + // under heavy parallel integration load. + for i := 0; i < 3; i++ { + _, _, errno := syscall.Syscall( + syscall.SYS_LINK, + uintptr(unsafe.Pointer(srcBytes)), + uintptr(unsafe.Pointer(dstBytes)), + 0, + ) + if errno == 0 { + return fmt.Errorf("expected ENOENT, but link succeeded") + } + time.Sleep(20 * time.Millisecond) + } + runtime.KeepAlive(srcBytes) + runtime.KeepAlive(dstBytes) + 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 { + 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("symlink: %w", errno) + } + return nil +} + +// rawReadlink calls readlink(2) via raw SYS_READLINK. +func rawReadlink(path string) error { + pathBytes, err := syscall.BytePtrFromString(path) + if err != nil { + return fmt.Errorf("path bytes: %w", err) + } + buf := make([]byte, 256) + _, _, errno := syscall.Syscall( + syscall.SYS_READLINK, + uintptr(unsafe.Pointer(pathBytes)), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(len(buf)), + ) + runtime.KeepAlive(pathBytes) + runtime.KeepAlive(buf) + if errno != 0 { + return fmt.Errorf("readlink: %w", errno) + } + return nil +} diff --git a/cmd/ioworkload/scenario_mmap.go b/cmd/ioworkload/scenario_mmap.go new file mode 100644 index 0000000..e7b9f02 --- /dev/null +++ b/cmd/ioworkload/scenario_mmap.go @@ -0,0 +1,110 @@ +package main + +import ( + "fmt" + "path/filepath" + "syscall" + "unsafe" +) + +// mmapBasic creates a file-backed shared mapping. +// mmap(2) allows closing the fd after mapping without invalidating the mapping. +func mmapBasic() error { + dir, cleanup, err := makeTempDir("mmap-basic") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "mmapfile.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT|syscall.O_TRUNC, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + defer syscall.Close(fd) + + data := []byte("mmap shared page data") + if _, err := syscall.Write(fd, data); err != nil { + return fmt.Errorf("write: %w", err) + } + + mapped, err := syscall.Mmap(fd, 0, len(data), syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) + if err != nil { + return fmt.Errorf("mmap: %w", err) + } + defer syscall.Munmap(mapped) + + copy(mapped[:4], []byte("MMAP")) + return nil +} + +// mmapMsyncSync maps a file and flushes modifications via msync(2). +// Per msync(2), callers should specify exactly one of MS_SYNC or MS_ASYNC. +func mmapMsyncSync() error { + dir, cleanup, err := makeTempDir("mmap-msync-sync") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "msyncfile.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT|syscall.O_TRUNC, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + defer syscall.Close(fd) + + data := []byte("msync shared page data") + if _, err := syscall.Write(fd, data); err != nil { + return fmt.Errorf("write: %w", err) + } + + mapped, err := syscall.Mmap(fd, 0, len(data), syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) + if err != nil { + return fmt.Errorf("mmap: %w", err) + } + defer syscall.Munmap(mapped) + + copy(mapped[:5], []byte("MSYNC")) + + _, _, errno := syscall.Syscall(syscall.SYS_MSYNC, uintptr(unsafe.Pointer(&mapped[0])), uintptr(len(mapped)), uintptr(syscall.MS_SYNC)) + if errno != 0 { + return fmt.Errorf("msync: %w", errno) + } + return nil +} + +// mmapMsyncInvalidFlags calls msync(2) with both MS_SYNC and MS_ASYNC. +// The kernel returns EINVAL, but enter_msync should still be captured. +func mmapMsyncInvalidFlags() error { + dir, cleanup, err := makeTempDir("mmap-msync-invalid-flags") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "msyncinvalidfile.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT|syscall.O_TRUNC, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + defer syscall.Close(fd) + + data := []byte("msync invalid flags data") + if _, err := syscall.Write(fd, data); err != nil { + return fmt.Errorf("write: %w", err) + } + + mapped, err := syscall.Mmap(fd, 0, len(data), syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED) + if err != nil { + return fmt.Errorf("mmap: %w", err) + } + defer syscall.Munmap(mapped) + + flags := syscall.MS_SYNC | syscall.MS_ASYNC + _, _, errno := syscall.Syscall(syscall.SYS_MSYNC, uintptr(unsafe.Pointer(&mapped[0])), uintptr(len(mapped)), uintptr(flags)) + if errno != syscall.EINVAL { + return fmt.Errorf("expected EINVAL from msync with both MS_SYNC|MS_ASYNC, got %v", errno) + } + return nil +} diff --git a/cmd/ioworkload/scenario_open.go b/cmd/ioworkload/scenario_open.go new file mode 100644 index 0000000..1aebec1 --- /dev/null +++ b/cmd/ioworkload/scenario_open.go @@ -0,0 +1,270 @@ +package main + +import ( + "fmt" + "os/exec" + "path/filepath" + "runtime" + "syscall" + "time" + "unsafe" +) + +// openBasic opens a file with O_RDWR|O_CREAT, then closes it. +func openBasic() error { + dir, cleanup, err := makeTempDir("open-basic") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "testfile.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + return syscall.Close(fd) +} + +// openCreat creates a file via raw SYS_CREAT. +// Go's syscall.Creat wraps Open which delegates to openat on amd64, +// so we use the raw syscall to actually exercise the creat tracepoint. +func openCreat() error { + dir, cleanup, err := makeTempDir("open-creat") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "creatfile.txt") + pathBytes, err := syscall.BytePtrFromString(path) + if err != nil { + return fmt.Errorf("path bytes: %w", err) + } + fd, _, errno := syscall.Syscall(syscall.SYS_CREAT, uintptr(unsafe.Pointer(pathBytes)), 0o644, 0) + runtime.KeepAlive(pathBytes) + if errno != 0 { + return fmt.Errorf("creat: %w", errno) + } + return syscall.Close(int(fd)) +} + +// openEnoent attempts to open a nonexistent file path. The openat syscall +// returns ENOENT, but ior should still capture the enter_openat tracepoint +// because the filename is read on entry before the syscall executes. +func openEnoent() error { + dir, cleanup, err := makeTempDir("open-enoent") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "nonexistent", "enoentfile.txt") + for i := 0; i < 5; i++ { + _, err = syscall.Open(path, syscall.O_RDONLY, 0) + if err == nil { + return fmt.Errorf("expected ENOENT, but open succeeded") + } + } + return nil +} + +// openRdonlyWrite opens a file O_RDONLY, then attempts to write to it. +// The write fails with EBADF, but ior should capture both the openat +// tracepoint and the write tracepoint. +func openRdonlyWrite() error { + dir, cleanup, err := makeTempDir("open-rdonly-write") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "rdonlyfile.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("create file: %w", err) + } + syscall.Close(fd) + + fd, err = syscall.Open(path, syscall.O_RDONLY, 0) + if err != nil { + return fmt.Errorf("open rdonly: %w", err) + } + defer syscall.Close(fd) + + _, err = syscall.Write(fd, []byte("should fail")) + if err == nil { + return fmt.Errorf("expected write to rdonly fd to fail") + } + return nil +} + +// openPidFilter spawns a child process that performs file I/O. Since ior +// filters by the workload PID, the child's I/O should NOT appear in results. +// The parent also performs its own open so the test can verify positive and +// negative expectations simultaneously. +func openPidFilter() error { + dir, cleanup, err := makeTempDir("open-pid-filter") + if err != nil { + return err + } + defer cleanup() + + // Parent opens a file (should be captured by ior). + parentPath := filepath.Join(dir, "parentfile.txt") + fd, err := syscall.Open(parentPath, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("parent open: %w", err) + } + syscall.Close(fd) + + // Spawn a child process that creates a file with a distinctive name. + childPath := filepath.Join(dir, "childfile.txt") + cmd := exec.Command("touch", childPath) + if err := cmd.Run(); err != nil { + return fmt.Errorf("child touch: %w", err) + } + return nil +} + +// openByHandleAt creates a file, resolves its handle via name_to_handle_at, +// then opens it via open_by_handle_at. Requires root (CAP_DAC_READ_SEARCH). +// LockOSThread prevents goroutine migration between the two syscalls so that +// ior sees the same TID for both and can correlate the path. +func openByHandleAt() error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + dir, cleanup, err := makeTempDir("open-by-handle-at") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "handlefile.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("close: %w", err) + } + + handle, mountFD, err := nameToHandleAt(dir, "handlefile.txt") + if err != nil { + return fmt.Errorf("name_to_handle_at: %w", err) + } + defer syscall.Close(mountFD) + + fd2, err := openByHandleAtSyscall(mountFD, handle, syscall.O_RDONLY) + if err != nil { + return fmt.Errorf("open_by_handle_at: %w", err) + } + return syscall.Close(fd2) +} + +// fileHandle matches the kernel's struct file_handle layout. +type fileHandle struct { + Size uint32 + Type int32 + // Handle bytes follow immediately after. +} + +const ( + sysNameToHandleAt = 303 + sysOpenByHandleAt = 304 +) + +// nameToHandleAt calls name_to_handle_at(2) and returns the file handle +// and the directory fd. The caller can pass this dirFD as the mount_fd +// argument to open_by_handle_at since any fd on the same filesystem works. +func nameToHandleAt(dirPath, name string) ([]byte, int, error) { + dirFD, err := syscall.Open(dirPath, syscall.O_RDONLY|syscall.O_DIRECTORY, 0) + if err != nil { + return nil, 0, fmt.Errorf("open dir: %w", err) + } + + nameBytes, err := syscall.BytePtrFromString(name) + if err != nil { + syscall.Close(dirFD) + return nil, 0, fmt.Errorf("name bytes: %w", err) + } + + // Start with a buffer large enough for most handles. + buf := make([]byte, unsafe.Sizeof(fileHandle{})+128) + fh := (*fileHandle)(unsafe.Pointer(&buf[0])) + fh.Size = uint32(len(buf) - int(unsafe.Sizeof(fileHandle{}))) + + var mountID int32 + _, _, errno := syscall.Syscall6( + sysNameToHandleAt, + uintptr(dirFD), + uintptr(unsafe.Pointer(nameBytes)), + uintptr(unsafe.Pointer(fh)), + uintptr(unsafe.Pointer(&mountID)), + 0, 0, + ) + if errno != 0 { + syscall.Close(dirFD) + return nil, 0, fmt.Errorf("syscall: %w", errno) + } + + handleLen := int(unsafe.Sizeof(fileHandle{})) + int(fh.Size) + handle := make([]byte, handleLen) + copy(handle, buf[:handleLen]) + + return handle, dirFD, nil +} + +// openByHandleAtSyscall calls open_by_handle_at(2). +func openByHandleAtSyscall(mountFD int, handle []byte, flags int) (int, error) { + fd, _, errno := syscall.Syscall( + sysOpenByHandleAt, + uintptr(mountFD), + uintptr(unsafe.Pointer(&handle[0])), + uintptr(flags), + ) + if errno != 0 { + return 0, fmt.Errorf("syscall: %w", errno) + } + return int(fd), nil +} + +// openDurationGap performs two openat syscalls for the same path and flags, +// separated by a deliberate sleep. Integration tests use this to assert that +// durationToPrev captures inter-syscall gaps for the same event key. +func openDurationGap() error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + dir, cleanup, err := makeTempDir("open-duration-gap") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "gap-shared.txt") + + // Repeat the same open/sleep/open pattern to make the gap observation robust + // under high test parallelism where individual events can occasionally drop. + for i := 0; i < 5; i++ { + fd1, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open first: %w", err) + } + if err := syscall.Close(fd1); err != nil { + return fmt.Errorf("close first: %w", err) + } + + time.Sleep(800 * time.Millisecond) + + fd2, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open second: %w", err) + } + if err := syscall.Close(fd2); err != nil { + return fmt.Errorf("close second: %w", err) + } + } + return nil +} diff --git a/cmd/ioworkload/scenario_pidfd.go b/cmd/ioworkload/scenario_pidfd.go new file mode 100644 index 0000000..2aafced --- /dev/null +++ b/cmd/ioworkload/scenario_pidfd.go @@ -0,0 +1,133 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "syscall" + "time" +) + +// pidfdGetfdSuccess duplicates an existing file descriptor through pidfd_getfd. +func pidfdGetfdSuccess() error { + dir, cleanup, err := makeTempDir("pidfd-getfd-success") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "pidfd-getfd-source.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open source: %w", err) + } + defer syscall.Close(fd) + + pidfd, err := pidfdOpen(os.Getpid(), 0) + if err != nil { + return fmt.Errorf("pidfd_open self: %w", err) + } + defer syscall.Close(pidfd) + + dupFd, err := pidfdGetfd(pidfd, fd, 0) + if err != nil { + return fmt.Errorf("pidfd_getfd: %w", err) + } + + if _, err := syscall.Write(dupFd, []byte("via pidfd_getfd")); err != nil { + syscall.Close(dupFd) + return fmt.Errorf("write dup fd: %w", err) + } + + // Keep the duplicated fd alive briefly so eventloop can resolve /proc fd path. + time.Sleep(500 * time.Millisecond) + if err := syscall.Close(dupFd); err != nil { + return fmt.Errorf("close dup fd: %w", err) + } + return nil +} + +// pidfdGetfdFailure performs a guaranteed-failing pidfd_getfd call while +// also probing a cross-process call that may fail under ptrace/Yama policy. +func pidfdGetfdFailure() error { + pidfd, err := pidfdOpen(os.Getpid(), 0) + if err != nil { + return fmt.Errorf("pidfd_open self: %w", err) + } + defer syscall.Close(pidfd) + + // Best-effort probe. Depending on kernel ptrace/Yama policy, this may fail + // with EPERM/EACCES; if it succeeds we close the returned fd and continue. + if initPidfd, err := pidfdOpen(1, 0); err == nil { + func() { + defer syscall.Close(initPidfd) + if probeFd, err := pidfdGetfd(initPidfd, 1, 0); err == nil { + syscall.Close(probeFd) + } + }() + } + + _, err = pidfdGetfd(pidfd, 99999, 0) + if err == nil { + return fmt.Errorf("expected pidfd_getfd with invalid source fd to fail") + } + return nil +} + +func pidfdOpen(pid int, flags uintptr) (int, error) { + syscallNr, err := pidfdOpenSyscallNr() + if err != nil { + return 0, err + } + fd, _, errno := syscall.Syscall(syscallNr, uintptr(pid), flags, 0) + if errno != 0 { + return 0, errno + } + return int(fd), nil +} + +func pidfdGetfd(pidfd int, targetFd int, flags uintptr) (int, error) { + syscallNr, err := pidfdGetfdSyscallNr() + if err != nil { + return 0, err + } + fd, _, errno := syscall.Syscall( + syscallNr, + uintptr(pidfd), + uintptr(targetFd), + flags, + ) + if errno != 0 { + return 0, errno + } + return int(fd), nil +} + +func pidfdOpenSyscallNr() (uintptr, error) { + return pidfdOpenSyscallNrForArch(runtime.GOARCH) +} + +func pidfdGetfdSyscallNr() (uintptr, error) { + return pidfdGetfdSyscallNrForArch(runtime.GOARCH) +} + +func pidfdOpenSyscallNrForArch(arch string) (uintptr, error) { + // Go's syscall package does not expose pidfd constants on all toolchains. + switch arch { + case "amd64", "arm64": + return 434, nil + default: + return 0, fmt.Errorf("pidfd_open syscall number not defined for GOARCH=%s", arch) + } +} + +func pidfdGetfdSyscallNrForArch(arch string) (uintptr, error) { + // Go's syscall package does not expose pidfd constants on all toolchains. + switch arch { + case "amd64", "arm64": + return 438, nil + default: + return 0, fmt.Errorf("pidfd_getfd syscall number not defined for GOARCH=%s", arch) + } +} diff --git a/cmd/ioworkload/scenario_pidfd_test.go b/cmd/ioworkload/scenario_pidfd_test.go new file mode 100644 index 0000000..5ee1002 --- /dev/null +++ b/cmd/ioworkload/scenario_pidfd_test.go @@ -0,0 +1,57 @@ +package main + +import "testing" + +func TestPidfdOpenSyscallNrForArch(t *testing.T) { + for _, tc := range []struct { + name string + arch string + want uintptr + wantErr bool + }{ + {name: "amd64", arch: "amd64", want: 434}, + {name: "arm64", arch: "arm64", want: 434}, + {name: "unsupported", arch: "riscv64", wantErr: true}, + } { + got, err := pidfdOpenSyscallNrForArch(tc.arch) + if tc.wantErr { + if err == nil { + t.Fatalf("%s: expected error", tc.name) + } + continue + } + if err != nil { + t.Fatalf("%s: unexpected error: %v", tc.name, err) + } + if got != tc.want { + t.Fatalf("%s: got %d, want %d", tc.name, got, tc.want) + } + } +} + +func TestPidfdGetfdSyscallNrForArch(t *testing.T) { + for _, tc := range []struct { + name string + arch string + want uintptr + wantErr bool + }{ + {name: "amd64", arch: "amd64", want: 438}, + {name: "arm64", arch: "arm64", want: 438}, + {name: "unsupported", arch: "riscv64", wantErr: true}, + } { + got, err := pidfdGetfdSyscallNrForArch(tc.arch) + if tc.wantErr { + if err == nil { + t.Fatalf("%s: expected error", tc.name) + } + continue + } + if err != nil { + t.Fatalf("%s: unexpected error: %v", tc.name, err) + } + if got != tc.want { + t.Fatalf("%s: got %d, want %d", tc.name, got, tc.want) + } + } +} diff --git a/cmd/ioworkload/scenario_readwrite.go b/cmd/ioworkload/scenario_readwrite.go new file mode 100644 index 0000000..c676b90 --- /dev/null +++ b/cmd/ioworkload/scenario_readwrite.go @@ -0,0 +1,263 @@ +package main + +import ( + "fmt" + "path/filepath" + "runtime" + "syscall" + "unsafe" +) + +// readwriteBasic opens a file, writes data, seeks to start, reads it back. +func readwriteBasic() error { + dir, cleanup, err := makeTempDir("readwrite-basic") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "rwfile.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) + + data := []byte("hello from ioworkload") + if _, err := syscall.Write(fd, data); err != nil { + return fmt.Errorf("write: %w", err) + } + if _, err := syscall.Seek(fd, 0, 0); err != nil { + return fmt.Errorf("seek: %w", err) + } + + buf := make([]byte, len(data)) + if _, err := syscall.Read(fd, buf); err != nil { + return fmt.Errorf("read: %w", err) + } + return nil +} + +// readwritePread opens a file, writes data, then reads it back via pread64. +func readwritePread() error { + dir, cleanup, err := makeTempDir("readwrite-pread") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "preadfile.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) + + data := []byte("pread test data") + if _, err := syscall.Write(fd, data); err != nil { + return fmt.Errorf("write: %w", err) + } + + buf := make([]byte, len(data)) + if _, err := syscall.Pread(fd, buf, 0); err != nil { + return fmt.Errorf("pread: %w", err) + } + return nil +} + +// readwritePwrite opens a file and writes data via pwrite64. +func readwritePwrite() error { + dir, cleanup, err := makeTempDir("readwrite-pwrite") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "pwritefile.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.Pwrite(fd, []byte("pwrite test data"), 0); err != nil { + return fmt.Errorf("pwrite: %w", err) + } + return nil +} + +// readwriteReadv opens a file, writes data, then reads it back via readv. +func readwriteReadv() error { + dir, cleanup, err := makeTempDir("readwrite-readv") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "readvfile.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) + + data := []byte("readv test data here") + if _, err := syscall.Write(fd, data); err != nil { + return fmt.Errorf("write: %w", err) + } + if _, err := syscall.Seek(fd, 0, 0); err != nil { + return fmt.Errorf("seek: %w", err) + } + + buf1 := make([]byte, 5) + buf2 := make([]byte, 15) + iovs := []syscall.Iovec{ + {Base: &buf1[0], Len: uint64(len(buf1))}, + {Base: &buf2[0], Len: uint64(len(buf2))}, + } + _, _, errno := syscall.Syscall(syscall.SYS_READV, uintptr(fd), uintptr(unsafe.Pointer(&iovs[0])), uintptr(len(iovs))) + runtime.KeepAlive(buf1) + runtime.KeepAlive(buf2) + if errno != 0 { + return fmt.Errorf("readv: %w", errno) + } + return nil +} + +// readwriteWritev opens a file and writes data via writev. +func readwriteWritev() error { + dir, cleanup, err := makeTempDir("readwrite-writev") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "writevfile.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) + + buf1 := []byte("writev ") + buf2 := []byte("test data") + iovs := []syscall.Iovec{ + {Base: &buf1[0], Len: uint64(len(buf1))}, + {Base: &buf2[0], Len: uint64(len(buf2))}, + } + _, _, errno := syscall.Syscall(syscall.SYS_WRITEV, uintptr(fd), uintptr(unsafe.Pointer(&iovs[0])), uintptr(len(iovs))) + runtime.KeepAlive(buf1) + runtime.KeepAlive(buf2) + if errno != 0 { + return fmt.Errorf("writev: %w", errno) + } + return nil +} + +// readwriteWronlyRead opens a file O_WRONLY, then attempts to read from it. +// The read fails with EBADF, but ior should capture the enter_read tracepoint +// because arguments are read on syscall entry before the kernel returns an error. +func readwriteWronlyRead() error { + dir, cleanup, err := makeTempDir("readwrite-wronly-read") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "wronlyfile.txt") + fd, err := syscall.Open(path, syscall.O_WRONLY|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + defer syscall.Close(fd) + + buf := make([]byte, 16) + _, err = syscall.Read(fd, buf) + if err == nil { + return fmt.Errorf("expected read from wronly fd to fail") + } + return nil +} + +// readwriteRdonlyWrite opens a file O_RDONLY, then attempts to write to it. +// The write fails with EBADF, but ior should capture the enter_write tracepoint +// because arguments are read on syscall entry before the kernel returns an error. +func readwriteRdonlyWrite() error { + dir, cleanup, err := makeTempDir("readwrite-rdonly-write") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "rdonlywritefile.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("create file: %w", err) + } + syscall.Close(fd) + + fd, err = syscall.Open(path, syscall.O_RDONLY, 0) + if err != nil { + return fmt.Errorf("open rdonly: %w", err) + } + defer syscall.Close(fd) + + _, err = syscall.Write(fd, []byte("should fail")) + if err == nil { + return fmt.Errorf("expected write to rdonly fd to fail") + } + return nil +} + +// readwritePreadInvalid calls pread64 with a negative offset (-1). +// The syscall fails with EINVAL, but ior should capture the enter_pread64 +// tracepoint because arguments are read on syscall entry. +func readwritePreadInvalid() error { + dir, cleanup, err := makeTempDir("readwrite-pread-invalid") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "preadinvalid.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.Write(fd, []byte("some data")); err != nil { + return fmt.Errorf("write: %w", err) + } + + buf := make([]byte, 16) + _, err = syscall.Pread(fd, buf, -1) + if err == nil { + return fmt.Errorf("expected pread with negative offset to fail") + } + return nil +} + +// readwritePwriteInvalid calls pwrite64 with a negative offset (-1). +// The syscall fails with EINVAL, but ior should capture the enter_pwrite64 +// tracepoint because arguments are read on syscall entry. +func readwritePwriteInvalid() error { + dir, cleanup, err := makeTempDir("readwrite-pwrite-invalid") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "pwriteinvalid.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) + + _, err = syscall.Pwrite(fd, []byte("should fail"), -1) + if err == nil { + return fmt.Errorf("expected pwrite with negative offset to fail") + } + return nil +} diff --git a/cmd/ioworkload/scenario_rename.go b/cmd/ioworkload/scenario_rename.go new file mode 100644 index 0000000..685157b --- /dev/null +++ b/cmd/ioworkload/scenario_rename.go @@ -0,0 +1,253 @@ +package main + +import ( + "fmt" + "path/filepath" + "runtime" + "syscall" + "unsafe" +) + +const ( + sysRenameat2 = 316 // SYS_RENAMEAT2 on amd64 + renameNoreplaceFlag = 1 // RENAME_NOREPLACE +) + +// renameBasic creates a file and renames it via rename(2). +// Uses raw SYS_RENAME because Go's syscall.Rename wraps renameat on amd64. +func renameBasic() error { + dir, cleanup, err := makeTempDir("rename-basic") + if err != nil { + return err + } + defer cleanup() + + oldPath := filepath.Join(dir, "oldname.txt") + newPath := filepath.Join(dir, "newname.txt") + + fd, err := syscall.Open(oldPath, 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) + } + + 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_RENAME, + uintptr(unsafe.Pointer(oldBytes)), + uintptr(unsafe.Pointer(newBytes)), + 0, + ) + runtime.KeepAlive(oldBytes) + runtime.KeepAlive(newBytes) + if errno != 0 { + return fmt.Errorf("rename: %w", errno) + } + return nil +} + +// renameRenameat creates a file and renames it via renameat(2). +func renameRenameat() error { + dir, cleanup, err := makeTempDir("rename-renameat") + if err != nil { + return err + } + defer cleanup() + + oldName := "renameat-old.txt" + newName := "renameat-new.txt" + path := filepath.Join(dir, oldName) + + 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("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) + + oldBytes, err := syscall.BytePtrFromString(oldName) + if err != nil { + return fmt.Errorf("old name bytes: %w", err) + } + newBytes, err := syscall.BytePtrFromString(newName) + if err != nil { + return fmt.Errorf("new name bytes: %w", err) + } + + _, _, errno := syscall.Syscall6( + syscall.SYS_RENAMEAT, + uintptr(dirFD), + uintptr(unsafe.Pointer(oldBytes)), + uintptr(dirFD), + uintptr(unsafe.Pointer(newBytes)), + 0, 0, + ) + runtime.KeepAlive(oldBytes) + runtime.KeepAlive(newBytes) + if errno != 0 { + return fmt.Errorf("renameat: %w", errno) + } + return nil +} + +// renameRenameat2 creates a file and renames it via renameat2(2) with no flags. +func renameRenameat2() error { + dir, cleanup, err := makeTempDir("rename-renameat2") + if err != nil { + return err + } + defer cleanup() + + oldName := "renameat2-old.txt" + newName := "renameat2-new.txt" + path := filepath.Join(dir, oldName) + + 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("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) + + oldBytes, err := syscall.BytePtrFromString(oldName) + if err != nil { + return fmt.Errorf("old name bytes: %w", err) + } + newBytes, err := syscall.BytePtrFromString(newName) + if err != nil { + return fmt.Errorf("new name bytes: %w", err) + } + + _, _, errno := syscall.Syscall6( + sysRenameat2, + uintptr(dirFD), + uintptr(unsafe.Pointer(oldBytes)), + uintptr(dirFD), + uintptr(unsafe.Pointer(newBytes)), + 0, // flags=0: plain rename + 0, + ) + runtime.KeepAlive(oldBytes) + runtime.KeepAlive(newBytes) + if errno != 0 { + return fmt.Errorf("renameat2: %w", errno) + } + return nil +} + +// renameEnoent attempts to rename a nonexistent file via raw SYS_RENAME. +// The syscall fails with ENOENT, but ior captures the tracepoint on entry. +func renameEnoent() error { + dir, cleanup, err := makeTempDir("rename-enoent") + if err != nil { + return err + } + defer cleanup() + + oldPath := filepath.Join(dir, "rename-enoent-missing.txt") + newPath := filepath.Join(dir, "rename-enoent-new.txt") + + 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) + } + + for i := 0; i < 5; i++ { + _, _, errno := syscall.Syscall( + syscall.SYS_RENAME, + uintptr(unsafe.Pointer(oldBytes)), + uintptr(unsafe.Pointer(newBytes)), + 0, + ) + runtime.KeepAlive(oldBytes) + runtime.KeepAlive(newBytes) + if errno == 0 { + return fmt.Errorf("expected ENOENT, but rename succeeded") + } + } + return nil +} + +// renameNoreplace creates two files, then attempts renameat2 with +// RENAME_NOREPLACE. Because the target already exists, the syscall fails +// with EEXIST, but ior captures the tracepoint on entry. +func renameNoreplace() error { + dir, cleanup, err := makeTempDir("rename-noreplace") + if err != nil { + return err + } + defer cleanup() + + srcName := "noreplace-src.txt" + dstName := "noreplace-dst.txt" + + for _, name := range []string{srcName, dstName} { + path := filepath.Join(dir, name) + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("create %s: %w", name, err) + } + if err := syscall.Close(fd); err != nil { + return fmt.Errorf("close %s: %w", name, 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) + + srcBytes, err := syscall.BytePtrFromString(srcName) + if err != nil { + return fmt.Errorf("src name bytes: %w", err) + } + dstBytes, err := syscall.BytePtrFromString(dstName) + if err != nil { + return fmt.Errorf("dst name bytes: %w", err) + } + + _, _, errno := syscall.Syscall6( + sysRenameat2, + uintptr(dirFD), + uintptr(unsafe.Pointer(srcBytes)), + uintptr(dirFD), + uintptr(unsafe.Pointer(dstBytes)), + renameNoreplaceFlag, + 0, + ) + runtime.KeepAlive(srcBytes) + runtime.KeepAlive(dstBytes) + if errno == 0 { + return fmt.Errorf("expected EEXIST, but renameat2 NOREPLACE succeeded") + } + return nil +} diff --git a/cmd/ioworkload/scenario_stat.go b/cmd/ioworkload/scenario_stat.go new file mode 100644 index 0000000..5d242c7 --- /dev/null +++ b/cmd/ioworkload/scenario_stat.go @@ -0,0 +1,286 @@ +package main + +import ( + "fmt" + "path/filepath" + "runtime" + "syscall" + "time" + "unsafe" +) + +const ( + sysStatx = 332 + rOK = 0x4 // R_OK + statxBasicMask = 0x07ff // STATX_BASIC_STATS + atFDCwd = -100 // AT_FDCWD + statRetryDelay = 20 * time.Millisecond +) + +// statBasic creates a file and stats it via raw SYS_STAT (newstat). +// We use the raw syscall because Go's syscall.Stat wraps newfstatat on amd64. +func statBasic() error { + dir, cleanup, err := makeTempDir("stat-basic") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "statfile.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + syscall.Close(fd) + + var stat syscall.Stat_t + pathBytes, err := syscall.BytePtrFromString(path) + if err != nil { + return fmt.Errorf("path bytes: %w", err) + } + _, _, errno := syscall.Syscall(syscall.SYS_STAT, uintptr(unsafe.Pointer(pathBytes)), uintptr(unsafe.Pointer(&stat)), 0) + runtime.KeepAlive(pathBytes) + runtime.KeepAlive(&stat) + if errno != 0 { + return fmt.Errorf("stat: %w", errno) + } + return nil +} + +// statFstat creates a file and stats it via raw SYS_FSTAT (newfstat). +// This is an fd_event, so ior resolves the path via its fd lookup table. +func statFstat() error { + dir, cleanup, err := makeTempDir("stat-fstat") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "fstatfile.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) + + var stat syscall.Stat_t + _, _, errno := syscall.Syscall(syscall.SYS_FSTAT, uintptr(fd), uintptr(unsafe.Pointer(&stat)), 0) + runtime.KeepAlive(&stat) + if errno != 0 { + return fmt.Errorf("fstat: %w", errno) + } + return nil +} + +// statLstat creates a file and stats it via raw SYS_LSTAT (newlstat). +// We use the raw syscall because Go's syscall.Lstat wraps newfstatat on amd64. +func statLstat() error { + dir, cleanup, err := makeTempDir("stat-lstat") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "lstatfile.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + syscall.Close(fd) + + var stat syscall.Stat_t + pathBytes, err := syscall.BytePtrFromString(path) + if err != nil { + return fmt.Errorf("path bytes: %w", err) + } + _, _, errno := syscall.Syscall(syscall.SYS_LSTAT, uintptr(unsafe.Pointer(pathBytes)), uintptr(unsafe.Pointer(&stat)), 0) + runtime.KeepAlive(pathBytes) + runtime.KeepAlive(&stat) + if errno != 0 { + return fmt.Errorf("lstat: %w", errno) + } + return nil +} + +// statNewfstatat creates a file and stats it via Go's syscall.Stat, which +// wraps SYS_NEWFSTATAT (fstatat with AT_FDCWD) on amd64. +func statNewfstatat() error { + dir, cleanup, err := makeTempDir("stat-newfstatat") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "fstatatfile.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + syscall.Close(fd) + + var stat syscall.Stat_t + if err := syscall.Stat(path, &stat); err != nil { + return fmt.Errorf("newfstatat: %w", err) + } + return nil +} + +// statStatx creates a file and stats it via raw statx(2) syscall. +func statStatx() error { + dir, cleanup, err := makeTempDir("stat-statx") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "statxfile.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + syscall.Close(fd) + + pathBytes, err := syscall.BytePtrFromString(path) + if err != nil { + return fmt.Errorf("path bytes: %w", err) + } + var buf [256]byte // statx struct is ~256 bytes + _, _, errno := syscall.Syscall6( + sysStatx, + ^uintptr(99), // AT_FDCWD (-100) + uintptr(unsafe.Pointer(pathBytes)), + 0, + statxBasicMask, + uintptr(unsafe.Pointer(&buf[0])), + 0, + ) + runtime.KeepAlive(pathBytes) + runtime.KeepAlive(buf) + if errno != 0 { + return fmt.Errorf("statx: %w", errno) + } + return nil +} + +// statAccess creates a file and checks access via raw SYS_ACCESS. +// We use the raw syscall because Go's syscall.Access wraps faccessat on amd64. +func statAccess() error { + dir, cleanup, err := makeTempDir("stat-access") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "accessfile.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + syscall.Close(fd) + + pathBytes, err := syscall.BytePtrFromString(path) + if err != nil { + return fmt.Errorf("path bytes: %w", err) + } + _, _, errno := syscall.Syscall(syscall.SYS_ACCESS, uintptr(unsafe.Pointer(pathBytes)), rOK, 0) + runtime.KeepAlive(pathBytes) + if errno != 0 { + return fmt.Errorf("access: %w", errno) + } + return nil +} + +// statFaccessat creates a file and checks access via faccessat(2). +// Go's syscall.Faccessat wraps SYS_FACCESSAT. +func statFaccessat() error { + dir, cleanup, err := makeTempDir("stat-faccessat") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "faccessatfile.txt") + fd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + syscall.Close(fd) + + if err := syscall.Faccessat(atFDCwd, path, uint32(rOK), 0); err != nil { + return fmt.Errorf("faccessat: %w", err) + } + return nil +} + +// statEnoent attempts to stat a nonexistent file via raw SYS_STAT. +// The syscall fails with ENOENT, but ior captures the enter_newstat +// tracepoint because the filename is read on entry. +func statEnoent() error { + dir, cleanup, err := makeTempDir("stat-enoent") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "stat-enoent-missing.txt") + pathBytes, err := syscall.BytePtrFromString(path) + if err != nil { + return fmt.Errorf("path bytes: %w", err) + } + var stat syscall.Stat_t + // Retry a few times to reduce dropped-event flakiness under high parallelism. + for i := 0; i < 5; i++ { + _, _, errno := syscall.Syscall(syscall.SYS_STAT, uintptr(unsafe.Pointer(pathBytes)), uintptr(unsafe.Pointer(&stat)), 0) + runtime.KeepAlive(pathBytes) + runtime.KeepAlive(&stat) + if errno == 0 { + return fmt.Errorf("expected ENOENT, but stat succeeded") + } + } + return nil +} + +// statAccessEnoent attempts to check access on a nonexistent file via raw +// SYS_ACCESS. The syscall fails with ENOENT, but ior captures the +// enter_access tracepoint because the path is read on entry. +// We use ENOENT instead of EACCES because integration tests run as root, +// which bypasses DAC permission checks. +func statAccessEnoent() error { + dir, cleanup, err := makeTempDir("stat-access-enoent") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "access-enoent-missing.txt") + pathBytes, err := syscall.BytePtrFromString(path) + if err != nil { + return fmt.Errorf("path bytes: %w", err) + } + for i := 0; i < 5; i++ { + _, _, errno := syscall.Syscall(syscall.SYS_ACCESS, uintptr(unsafe.Pointer(pathBytes)), rOK, 0) + runtime.KeepAlive(pathBytes) + if errno == 0 { + return fmt.Errorf("expected ENOENT, but access succeeded") + } + } + return nil +} + +// statFstatEbadf calls raw SYS_FSTAT on an invalid fd (99999). +// The syscall fails with EBADF, but ior captures the enter_newfstat +// tracepoint because it is recorded on syscall entry. +func statFstatEbadf() error { + var stat syscall.Stat_t + for i := 0; i < 20; i++ { + _, _, errno := syscall.Syscall(syscall.SYS_FSTAT, 99999, uintptr(unsafe.Pointer(&stat)), 0) + runtime.KeepAlive(&stat) + if errno == 0 { + return fmt.Errorf("expected EBADF, but fstat succeeded") + } + if i < 19 { + time.Sleep(statRetryDelay) + } + } + return nil +} diff --git a/cmd/ioworkload/scenario_sync.go b/cmd/ioworkload/scenario_sync.go new file mode 100644 index 0000000..df1c59c --- /dev/null +++ b/cmd/ioworkload/scenario_sync.go @@ -0,0 +1,137 @@ +package main + +import ( + "fmt" + "path/filepath" + "syscall" +) + +// syncBasic opens a file, writes data, and fsyncs it. +func syncBasic() error { + dir, cleanup, err := makeTempDir("sync-basic") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "syncfile.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.Write(fd, []byte("sync me")); err != nil { + return fmt.Errorf("write: %w", err) + } + return syscall.Fsync(fd) +} + +// syncFdatasync opens a file, writes data, and fdatasyncs it. +func syncFdatasync() error { + dir, cleanup, err := makeTempDir("sync-fdatasync") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "fdatasyncfile.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.Write(fd, []byte("fdatasync me")); err != nil { + return fmt.Errorf("write: %w", err) + } + return syscall.Fdatasync(fd) +} + +// syncSync calls sync(2) to flush all filesystem caches. +// sync is a null_event with no file arguments. +func syncSync() error { + syscall.Sync() + return nil +} + +// syncSyncFileRange opens a file, writes data, then calls sync_file_range(2). +func syncSyncFileRange() error { + dir, cleanup, err := makeTempDir("sync-sync-file-range") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "syncrangefile.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) + + data := []byte("sync file range data") + if _, err := syscall.Write(fd, data); err != nil { + return fmt.Errorf("write: %w", err) + } + return syscall.SyncFileRange(fd, 0, int64(len(data)), 0) +} + +// syncSyncFileRangeToEOF calls sync_file_range(2) with nbytes=0. +// Per sync_file_range(2), nbytes=0 means "sync from offset through end-of-file". +func syncSyncFileRangeToEOF() error { + dir, cleanup, err := makeTempDir("sync-sync-file-range-to-eof") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "syncrangeeoffile.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.Write(fd, []byte("sync file range to eof")); err != nil { + return fmt.Errorf("write: %w", err) + } + + return syscall.SyncFileRange(fd, 0, 0, 0) +} + +// syncFsyncEbadf calls fsync on an invalid fd. +// The syscall fails with EBADF, but ior captures the enter_fsync tracepoint. +func syncFsyncEbadf() error { + for i := 0; i < 5; i++ { + _, _, errno := syscall.Syscall(syscall.SYS_FSYNC, 99999, 0, 0) + if errno == 0 { + return fmt.Errorf("expected EBADF, but fsync succeeded") + } + } + return nil +} + +// syncFdatasyncEbadf calls fdatasync on an invalid fd. +// The syscall fails with EBADF, but ior captures the enter_fdatasync tracepoint. +func syncFdatasyncEbadf() error { + for i := 0; i < 5; i++ { + _, _, errno := syscall.Syscall(syscall.SYS_FDATASYNC, 99999, 0, 0) + if errno == 0 { + return fmt.Errorf("expected EBADF, but fdatasync succeeded") + } + } + return nil +} + +// syncFileRangeEbadf calls sync_file_range on an invalid fd. +// The syscall fails with EBADF, but ior captures the enter_sync_file_range tracepoint. +func syncFileRangeEbadf() error { + for i := 0; i < 5; i++ { + _, _, errno := syscall.Syscall6(syscall.SYS_SYNC_FILE_RANGE, 99999, 0, 0, 0, 0, 0) + if errno == 0 { + return fmt.Errorf("expected EBADF, but sync_file_range succeeded") + } + } + return nil +} diff --git a/cmd/ioworkload/scenario_truncate.go b/cmd/ioworkload/scenario_truncate.go new file mode 100644 index 0000000..04288d5 --- /dev/null +++ b/cmd/ioworkload/scenario_truncate.go @@ -0,0 +1,93 @@ +package main + +import ( + "fmt" + "path/filepath" + "runtime" + "syscall" + "unsafe" +) + +// truncateBasic opens a file, writes data, then truncates it via +// syscall.Truncate which uses SYS_TRUNCATE directly on amd64 (path-based). +func truncateBasic() error { + dir, cleanup, err := makeTempDir("truncate-basic") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "truncfile.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.Write(fd, []byte("truncate this content")); err != nil { + syscall.Close(fd) + return fmt.Errorf("write: %w", err) + } + syscall.Close(fd) + + return syscall.Truncate(path, 5) +} + +// truncateFtruncate opens a file, writes data, then truncates it via +// syscall.Ftruncate which uses SYS_FTRUNCATE directly on amd64 (fd-based). +func truncateFtruncate() error { + dir, cleanup, err := makeTempDir("truncate-ftruncate") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "ftruncfile.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.Write(fd, []byte("ftruncate this content")); err != nil { + return fmt.Errorf("write: %w", err) + } + return syscall.Ftruncate(fd, 5) +} + +// truncateEnoent attempts to truncate a nonexistent file via raw SYS_TRUNCATE. +// The syscall fails with ENOENT, but ior captures the enter_truncate +// tracepoint because the path is read on entry. +func truncateEnoent() error { + dir, cleanup, err := makeTempDir("truncate-enoent") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "truncate-enoent-missing.txt") + pathBytes, err := syscall.BytePtrFromString(path) + if err != nil { + return fmt.Errorf("path bytes: %w", err) + } + // Retry a few times to make this test resilient under high integration + // parallelism where a single failed syscall event can be dropped. + for i := 0; i < 5; i++ { + _, _, errno := syscall.Syscall(syscall.SYS_TRUNCATE, uintptr(unsafe.Pointer(pathBytes)), 0, 0) + runtime.KeepAlive(pathBytes) + if errno == 0 { + return fmt.Errorf("expected ENOENT, but truncate succeeded") + } + } + return nil +} + +// truncateFtruncateEbadf calls raw SYS_FTRUNCATE on an invalid fd (99999). +// The syscall fails with EBADF, but ior captures the enter_ftruncate +// tracepoint because it is recorded on syscall entry. +func truncateFtruncateEbadf() error { + _, _, errno := syscall.Syscall(syscall.SYS_FTRUNCATE, 99999, 0, 0) + if errno == 0 { + return fmt.Errorf("expected EBADF, but ftruncate succeeded") + } + return nil +} diff --git a/cmd/ioworkload/scenario_unlink.go b/cmd/ioworkload/scenario_unlink.go new file mode 100644 index 0000000..ea73b10 --- /dev/null +++ b/cmd/ioworkload/scenario_unlink.go @@ -0,0 +1,193 @@ +package main + +import ( + "fmt" + "path/filepath" + "runtime" + "syscall" + "unsafe" +) + +// unlinkBasic creates a file and unlinks it via raw SYS_UNLINK. +// We use the raw syscall because Go's syscall.Unlink wraps unlinkat on amd64. +func unlinkBasic() error { + dir, cleanup, err := makeTempDir("unlink-basic") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "unlinkme.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("close: %w", err) + } + + pathBytes, err := syscall.BytePtrFromString(path) + if err != nil { + return fmt.Errorf("path bytes: %w", err) + } + _, _, errno := syscall.Syscall(syscall.SYS_UNLINK, uintptr(unsafe.Pointer(pathBytes)), 0, 0) + runtime.KeepAlive(pathBytes) + if errno != 0 { + return fmt.Errorf("unlink: %w", errno) + } + return nil +} + +// unlinkUnlinkat creates a file and unlinks it via unlinkat(2). +func unlinkUnlinkat() error { + dir, cleanup, err := makeTempDir("unlink-unlinkat") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "unlinkat-file.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("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("unlinkat-file.txt") + if err != nil { + return fmt.Errorf("name bytes: %w", err) + } + _, _, errno := syscall.Syscall(syscall.SYS_UNLINKAT, uintptr(dirFD), uintptr(unsafe.Pointer(nameBytes)), 0) + runtime.KeepAlive(nameBytes) + if errno != 0 { + return fmt.Errorf("unlinkat: %w", errno) + } + return nil +} + +// unlinkRmdir creates a directory and removes it via raw SYS_RMDIR. +// We use the raw syscall because Go's syscall.Rmdir wraps unlinkat on amd64. +func unlinkRmdir() error { + dir, cleanup, err := makeTempDir("unlink-rmdir") + if err != nil { + return err + } + defer cleanup() + + // Retry with fresh paths to avoid a single one-shot syscall that can race + // tracepoint attach during parallel integration test startup. + for i := 0; i < 5; i++ { + subDir := filepath.Join(dir, fmt.Sprintf("rmdir-me-%d", i)) + if err := syscall.Mkdir(subDir, 0o755); err != nil { + return fmt.Errorf("mkdir: %w", err) + } + + pathBytes, err := syscall.BytePtrFromString(subDir) + if err != nil { + return fmt.Errorf("path bytes: %w", err) + } + _, _, errno := syscall.Syscall(syscall.SYS_RMDIR, uintptr(unsafe.Pointer(pathBytes)), 0, 0) + runtime.KeepAlive(pathBytes) + if errno != 0 { + return fmt.Errorf("rmdir: %w", errno) + } + } + return nil +} + +// unlinkEnoent attempts to unlink a nonexistent file via raw SYS_UNLINK. +// The syscall fails with ENOENT, but ior captures the tracepoint on entry. +func unlinkEnoent() error { + dir, cleanup, err := makeTempDir("unlink-enoent") + if err != nil { + return err + } + defer cleanup() + + path := filepath.Join(dir, "unlink-enoent-missing.txt") + pathBytes, err := syscall.BytePtrFromString(path) + if err != nil { + return fmt.Errorf("path bytes: %w", err) + } + for i := 0; i < 5; i++ { + _, _, errno := syscall.Syscall(syscall.SYS_UNLINK, uintptr(unsafe.Pointer(pathBytes)), 0, 0) + runtime.KeepAlive(pathBytes) + if errno == 0 { + return fmt.Errorf("expected ENOENT, but unlink succeeded") + } + } + return nil +} + +// unlinkRmdirNotempty attempts to rmdir a non-empty directory via raw SYS_RMDIR. +// The syscall fails with ENOTEMPTY, but ior captures the tracepoint on entry. +func unlinkRmdirNotempty() error { + dir, cleanup, err := makeTempDir("unlink-rmdir-notempty") + if err != nil { + return err + } + defer cleanup() + + subDir := filepath.Join(dir, "rmdir-notempty") + if err := syscall.Mkdir(subDir, 0o755); err != nil { + return fmt.Errorf("mkdir: %w", err) + } + + // Create a file inside so the directory is non-empty. + filePath := filepath.Join(subDir, "blocker.txt") + fd, err := syscall.Open(filePath, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("create blocker: %w", err) + } + if err := syscall.Close(fd); err != nil { + return fmt.Errorf("close blocker: %w", err) + } + + pathBytes, err := syscall.BytePtrFromString(subDir) + if err != nil { + return fmt.Errorf("path bytes: %w", err) + } + _, _, errno := syscall.Syscall(syscall.SYS_RMDIR, uintptr(unsafe.Pointer(pathBytes)), 0, 0) + runtime.KeepAlive(pathBytes) + if errno == 0 { + return fmt.Errorf("expected ENOTEMPTY, but rmdir succeeded") + } + return nil +} + +// unlinkUnlinkatEnoent attempts to unlinkat a nonexistent file. +// The syscall fails with ENOENT, but ior captures the tracepoint on entry. +func unlinkUnlinkatEnoent() error { + dir, cleanup, err := makeTempDir("unlink-unlinkat-enoent") + if err != nil { + return err + } + defer cleanup() + + 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("unlinkat-enoent-missing.txt") + if err != nil { + return fmt.Errorf("name bytes: %w", err) + } + for i := 0; i < 5; i++ { + _, _, errno := syscall.Syscall(syscall.SYS_UNLINKAT, uintptr(dirFD), uintptr(unsafe.Pointer(nameBytes)), 0) + runtime.KeepAlive(nameBytes) + if errno == 0 { + return fmt.Errorf("expected ENOENT, but unlinkat succeeded") + } + } + return nil +} diff --git a/cmd/ioworkload/scenarios.go b/cmd/ioworkload/scenarios.go new file mode 100644 index 0000000..6910314 --- /dev/null +++ b/cmd/ioworkload/scenarios.go @@ -0,0 +1,118 @@ +package main + +import ( + "fmt" + "os" +) + +// scenarios maps scenario names to their execution functions. +var scenarios = map[string]func() error{ + "crash": crash, + "open-basic": openBasic, + "open-creat": openCreat, + "open-by-handle-at": openByHandleAt, + "open-duration-gap": openDurationGap, + "open-enoent": openEnoent, + "open-rdonly-write": openRdonlyWrite, + "open-pid-filter": openPidFilter, + "readwrite-basic": readwriteBasic, + "readwrite-pread": readwritePread, + "readwrite-pwrite": readwritePwrite, + "readwrite-readv": readwriteReadv, + "readwrite-writev": readwriteWritev, + "readwrite-wronly-read": readwriteWronlyRead, + "readwrite-rdonly-write": readwriteRdonlyWrite, + "readwrite-pread-invalid": readwritePreadInvalid, + "readwrite-pwrite-invalid": readwritePwriteInvalid, + "close-basic": closeBasic, + "close-range": closeRange, + "close-invalid-fd": closeInvalidFd, + "close-double-close": closeDoubleClose, + "close-range-empty": closeRangeEmpty, + "dup-basic": dupBasic, + "dup-dup2": dupDup2, + "dup-dup3": dupDup3, + "dup-invalid-fd": dupInvalidFd, + "dup2-same-fd": dup2SameFd, + "dup3-invalid-flags": dup3InvalidFlags, + "fcntl-dupfd": fcntlDupfd, + "fcntl-setfl": fcntlSetfl, + "fcntl-dupfd-cloexec": fcntlDupfdCloexec, + "fcntl-invalid-fd": fcntlInvalidFd, + "fcntl-dupfd-max": fcntlDupfdMax, + "rename-basic": renameBasic, + "rename-renameat": renameRenameat, + "rename-renameat2": renameRenameat2, + "rename-enoent": renameEnoent, + "rename-noreplace": renameNoreplace, + "link-basic": linkBasic, + "link-linkat": linkLinkat, + "link-symlinkat": linkSymlinkat, + "link-readlinkat": linkReadlinkat, + "link-enoent": linkEnoent, + "link-symlink-eexist": linkSymlinkEexist, + "link-readlinkat-einval": linkReadlinkatEinval, + "unlink-basic": unlinkBasic, + "unlink-unlinkat": unlinkUnlinkat, + "unlink-rmdir": unlinkRmdir, + "unlink-enoent": unlinkEnoent, + "unlink-rmdir-notempty": unlinkRmdirNotempty, + "unlink-unlinkat-enoent": unlinkUnlinkatEnoent, + "dir-basic": dirBasic, + "dir-mkdirat": dirMkdirat, + "dir-chdir": dirChdir, + "dir-getcwd": dirGetcwd, + "dir-getdents": dirGetdents, + "dir-mkdir-eexist": dirMkdirEexist, + "dir-chdir-enoent": dirChdirEnoent, + "dir-getdents-ebadf": dirGetdentsEbadf, + "stat-basic": statBasic, + "stat-fstat": statFstat, + "stat-lstat": statLstat, + "stat-newfstatat": statNewfstatat, + "stat-statx": statStatx, + "stat-access": statAccess, + "stat-faccessat": statFaccessat, + "stat-enoent": statEnoent, + "stat-access-enoent": statAccessEnoent, + "stat-fstat-ebadf": statFstatEbadf, + "sync-basic": syncBasic, + "sync-fdatasync": syncFdatasync, + "sync-sync": syncSync, + "sync-sync-file-range": syncSyncFileRange, + "sync-sync-file-range-to-eof": syncSyncFileRangeToEOF, + "sync-fsync-ebadf": syncFsyncEbadf, + "sync-fdatasync-ebadf": syncFdatasyncEbadf, + "sync-file-range-ebadf": syncFileRangeEbadf, + "mmap-basic": mmapBasic, + "mmap-msync-sync": mmapMsyncSync, + "mmap-msync-invalid-flags": mmapMsyncInvalidFlags, + "copy-file-range-basic": copyFileRangeBasic, + "copy-file-range-bad-dst-fd": copyFileRangeBadDstFd, + "truncate-basic": truncateBasic, + "truncate-ftruncate": truncateFtruncate, + "truncate-enoent": truncateEnoent, + "truncate-ftruncate-ebadf": truncateFtruncateEbadf, + "pidfd-getfd-success": pidfdGetfdSuccess, + "pidfd-getfd-failure": pidfdGetfdFailure, + "iouring-setup": iouringSetup, + "iouring-enter": iouringEnter, + "iouring-register": iouringRegister, + "iouring-enter-ebadf": iouringEnterEbadf, + "iouring-register-ebadf": iouringRegisterEbadf, +} + +func makeTempDir(prefix string) (string, func(), error) { + dir, err := os.MkdirTemp("", fmt.Sprintf("ioworkload-%s-", prefix)) + if err != nil { + return "", nil, fmt.Errorf("create temp dir: %w", err) + } + cleanup := func() { os.RemoveAll(dir) } + return dir, cleanup, nil +} + +// crash simulates a workload that fails with a non-zero exit code. +// Used to verify the test harness handles workload failures gracefully. +func crash() error { + return fmt.Errorf("intentional crash for testing") +} |
