diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-21 21:20:30 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-21 21:20:30 +0200 |
| commit | 69395ffff024254b114eeba543af68cc6ca77f0c (patch) | |
| tree | 77d8dea1d2bab810e66ae7fb19bccc1d56efe970 /integrationtests | |
| parent | 04500a92433396ca17cb879f4a40e4399dc06ff9 (diff) | |
Add negative integration tests for link syscalls (task 348)
Add three negative scenarios and tests:
- link-enoent: hard link to nonexistent source (ENOENT) via raw SYS_LINK
- link-symlink-eexist: symlink where target already exists (EEXIST) via raw SYS_SYMLINK
- link-readlinkat-einval: readlinkat on non-symlink file (EINVAL) via SYS_READLINKAT
All use raw syscalls to hit exact tracepoints. ior captures on syscall
entry, so even failed syscalls have their arguments recorded.
Amp-Thread-ID: https://ampcode.com/threads/T-019c81a2-693b-716f-9ed2-25a674f1fc9a
Co-authored-by: Amp <amp@ampcode.com>
Diffstat (limited to 'integrationtests')
| -rw-r--r-- | integrationtests/cmd/ioworkload/scenarios.go | 132 | ||||
| -rw-r--r-- | integrationtests/link_test.go | 33 |
2 files changed, 163 insertions, 2 deletions
diff --git a/integrationtests/cmd/ioworkload/scenarios.go b/integrationtests/cmd/ioworkload/scenarios.go index a186b08..4a96a8b 100644 --- a/integrationtests/cmd/ioworkload/scenarios.go +++ b/integrationtests/cmd/ioworkload/scenarios.go @@ -52,8 +52,11 @@ var scenarios = map[string]func() error{ "link-basic": linkBasic, "link-linkat": linkLinkat, "link-symlinkat": linkSymlinkat, - "link-readlinkat": linkReadlinkat, - "unlink-basic": unlinkBasic, + "link-readlinkat": linkReadlinkat, + "link-enoent": linkEnoent, + "link-symlink-eexist": linkSymlinkEexist, + "link-readlinkat-einval": linkReadlinkatEinval, + "unlink-basic": unlinkBasic, "unlink-unlinkat": unlinkUnlinkat, "unlink-rmdir": unlinkRmdir, "dir-basic": dirBasic, @@ -1320,6 +1323,131 @@ func linkReadlinkat() error { return nil } +// linkEnoent attempts to hard link a nonexistent source via raw SYS_LINK. +// The syscall fails with ENOENT, but ior captures the enter_link tracepoint +// because arguments are read on syscall entry. +func linkEnoent() error { + dir, cleanup, err := makeTempDir("link-enoent") + if err != nil { + return err + } + defer cleanup() + + srcPath := filepath.Join(dir, "link-enoent-missing.txt") + dstPath := filepath.Join(dir, "link-enoent-dst.txt") + + srcBytes, err := syscall.BytePtrFromString(srcPath) + if err != nil { + return fmt.Errorf("src path bytes: %w", err) + } + dstBytes, err := syscall.BytePtrFromString(dstPath) + if err != nil { + return fmt.Errorf("dst path bytes: %w", err) + } + + _, _, errno := syscall.Syscall( + syscall.SYS_LINK, + uintptr(unsafe.Pointer(srcBytes)), + uintptr(unsafe.Pointer(dstBytes)), + 0, + ) + runtime.KeepAlive(srcBytes) + runtime.KeepAlive(dstBytes) + if errno == 0 { + return fmt.Errorf("expected ENOENT, but link succeeded") + } + return nil +} + +// linkSymlinkEexist creates a regular file, then attempts to create a symlink +// at the same path via raw SYS_SYMLINK. The syscall fails with EEXIST because +// the link path already exists, but ior captures the enter_symlink tracepoint. +func linkSymlinkEexist() error { + dir, cleanup, err := makeTempDir("link-symlink-eexist") + if err != nil { + return err + } + defer cleanup() + + existingPath := filepath.Join(dir, "symlink-eexist.txt") + fd, err := syscall.Open(existingPath, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + if err := syscall.Close(fd); err != nil { + return fmt.Errorf("close: %w", err) + } + + targetBytes, err := syscall.BytePtrFromString("/tmp/dummy-target") + if err != nil { + return fmt.Errorf("target bytes: %w", err) + } + linkBytes, err := syscall.BytePtrFromString(existingPath) + if err != nil { + return fmt.Errorf("link path bytes: %w", err) + } + + _, _, errno := syscall.Syscall( + syscall.SYS_SYMLINK, + uintptr(unsafe.Pointer(targetBytes)), + uintptr(unsafe.Pointer(linkBytes)), + 0, + ) + runtime.KeepAlive(targetBytes) + runtime.KeepAlive(linkBytes) + if errno == 0 { + return fmt.Errorf("expected EEXIST, but symlink succeeded") + } + return nil +} + +// linkReadlinkatEinval creates a regular file and calls readlinkat(2) on it. +// The syscall fails with EINVAL because the path is not a symlink, but ior +// captures the enter_readlinkat tracepoint on syscall entry. +func linkReadlinkatEinval() error { + dir, cleanup, err := makeTempDir("link-readlinkat-einval") + if err != nil { + return err + } + defer cleanup() + + regularFile := "readlinkat-einval.txt" + regularPath := filepath.Join(dir, regularFile) + fd, err := syscall.Open(regularPath, syscall.O_RDWR|syscall.O_CREAT, 0o644) + if err != nil { + return fmt.Errorf("open: %w", err) + } + if err := syscall.Close(fd); err != nil { + return fmt.Errorf("close: %w", err) + } + + dirFD, err := syscall.Open(dir, syscall.O_RDONLY|syscall.O_DIRECTORY, 0) + if err != nil { + return fmt.Errorf("open dir: %w", err) + } + defer syscall.Close(dirFD) + + nameBytes, err := syscall.BytePtrFromString(regularFile) + if err != nil { + return fmt.Errorf("name bytes: %w", err) + } + buf := make([]byte, 256) + _, _, errno := syscall.Syscall6( + syscall.SYS_READLINKAT, + uintptr(dirFD), + uintptr(unsafe.Pointer(nameBytes)), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(len(buf)), + 0, 0, + ) + runtime.KeepAlive(nameBytes) + runtime.KeepAlive(buf) + if errno == 0 { + return fmt.Errorf("expected EINVAL, but readlinkat succeeded") + } + return nil +} + // 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 { diff --git a/integrationtests/link_test.go b/integrationtests/link_test.go index fcebffb..4f92f6e 100644 --- a/integrationtests/link_test.go +++ b/integrationtests/link_test.go @@ -57,3 +57,36 @@ func TestLinkReadlinkat(t *testing.T) { }, }) } + +func TestLinkEnoent(t *testing.T) { + runScenario(t, "link-enoent", []ExpectedEvent{ + { + PathContains: "link-enoent-missing.txt", + Tracepoint: "enter_link", + Comm: "ioworkload", + MinCount: 1, + }, + }) +} + +func TestLinkSymlinkEexist(t *testing.T) { + runScenario(t, "link-symlink-eexist", []ExpectedEvent{ + { + PathContains: "symlink-eexist.txt", + Tracepoint: "enter_symlink", + Comm: "ioworkload", + MinCount: 1, + }, + }) +} + +func TestLinkReadlinkatEinval(t *testing.T) { + runScenario(t, "link-readlinkat-einval", []ExpectedEvent{ + { + PathContains: "readlinkat-einval.txt", + Tracepoint: "enter_readlinkat", + Comm: "ioworkload", + MinCount: 1, + }, + }) +} |
