summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-21 21:25:27 +0200
committerPaul Buetow <paul@buetow.org>2026-02-21 21:25:27 +0200
commitb443f001cce8662a7fdccd4e0e7f707f7b2b47b8 (patch)
tree017138e189d77bd537a9d4a0d22f28fb2aa5147e
parent69395ffff024254b114eeba543af68cc6ca77f0c (diff)
Add negative integration tests for unlink syscalls (task 348)
Add three negative test scenarios for unlink syscall family: - unlink-enoent: SYS_UNLINK on nonexistent file (ENOENT) - unlink-rmdir-notempty: SYS_RMDIR on non-empty directory (ENOTEMPTY) - unlink-unlinkat-enoent: SYS_UNLINKAT on nonexistent file (ENOENT) All scenarios use raw syscalls to hit exact tracepoints and verify ior captures them on entry even when the kernel returns an error. Amp-Thread-ID: https://ampcode.com/threads/T-019c81a6-6612-7247-9d54-6da5b63a38b4 Co-authored-by: Amp <amp@ampcode.com>
-rw-r--r--integrationtests/cmd/ioworkload/scenarios.go92
-rw-r--r--integrationtests/unlink_test.go33
2 files changed, 123 insertions, 2 deletions
diff --git a/integrationtests/cmd/ioworkload/scenarios.go b/integrationtests/cmd/ioworkload/scenarios.go
index 4a96a8b..b4a153b 100644
--- a/integrationtests/cmd/ioworkload/scenarios.go
+++ b/integrationtests/cmd/ioworkload/scenarios.go
@@ -57,8 +57,11 @@ var scenarios = map[string]func() error{
"link-symlink-eexist": linkSymlinkEexist,
"link-readlinkat-einval": linkReadlinkatEinval,
"unlink-basic": unlinkBasic,
- "unlink-unlinkat": unlinkUnlinkat,
- "unlink-rmdir": unlinkRmdir,
+ "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,
@@ -1539,6 +1542,91 @@ func unlinkRmdir() error {
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)
+ }
+ _, _, 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)
+ }
+ _, _, 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
+}
+
// 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.
diff --git a/integrationtests/unlink_test.go b/integrationtests/unlink_test.go
index 942f43d..3e61991 100644
--- a/integrationtests/unlink_test.go
+++ b/integrationtests/unlink_test.go
@@ -34,3 +34,36 @@ func TestUnlinkRmdir(t *testing.T) {
},
})
}
+
+func TestUnlinkEnoent(t *testing.T) {
+ runScenario(t, "unlink-enoent", []ExpectedEvent{
+ {
+ PathContains: "unlink-enoent-missing.txt",
+ Tracepoint: "enter_unlink",
+ Comm: "ioworkload",
+ MinCount: 1,
+ },
+ })
+}
+
+func TestUnlinkRmdirNotempty(t *testing.T) {
+ runScenario(t, "unlink-rmdir-notempty", []ExpectedEvent{
+ {
+ PathContains: "rmdir-notempty",
+ Tracepoint: "enter_rmdir",
+ Comm: "ioworkload",
+ MinCount: 1,
+ },
+ })
+}
+
+func TestUnlinkUnlinkatEnoent(t *testing.T) {
+ runScenario(t, "unlink-unlinkat-enoent", []ExpectedEvent{
+ {
+ PathContains: "unlinkat-enoent-missing.txt",
+ Tracepoint: "enter_unlinkat",
+ Comm: "ioworkload",
+ MinCount: 1,
+ },
+ })
+}