diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-30 17:00:49 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-30 17:00:49 +0300 |
| commit | d071a75f44bc14dce364142483b072272c81313e (patch) | |
| tree | d07851904ed25d04465a00115051ab16f3a5ebeb /internal | |
| parent | 2e018ccde462cd9083cfaf9e74c50cec19518815 (diff) | |
test(getcwd): lock in KindNull enter + exit-time cwd resolution
Audit of the getcwd(2) tracing path. getcwd's args[0] is a char *buf
OUTPUT buffer: the kernel writes the absolute cwd path into it and the
contents are only valid AFTER the syscall returns. Reading it at enter
would capture an empty/garbage string, so getcwd is correctly KindNull
at enter and the cwd is resolved at EXIT from /proc/<tid>/cwd when the
return value is positive (handleNullExit). Family FS, docs and drift
tests already aligned; no behavior change required.
Add lock-in tests pinning the correct behavior:
- generate: strengthen TestClassifyNullGetcwd to assert the enter kind
is never KindPathname/KindName and no pathname field is captured;
add TestClassifyByFieldGetcwdBufNotPath proving the generic field
classifier never treats char *buf as a path (defense-in-depth).
- eventloop: add GetcwdFailureEventTest asserting that a failed getcwd
(negative errno, e.g. -ERANGE) attaches no cwd path, and document the
output-buffer nuance in the success-case test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/eventloop_test.go | 31 | ||||
| -rw-r--r-- | internal/generate/classify_test.go | 27 |
2 files changed, 58 insertions, 0 deletions
diff --git a/internal/eventloop_test.go b/internal/eventloop_test.go index 98d7696..3d256be 100644 --- a/internal/eventloop_test.go +++ b/internal/eventloop_test.go @@ -66,6 +66,7 @@ func TestEventloop(t *testing.T) { // NullEvent tests "SyncEventTest": makeSyncEventTestData(t), "GetcwdEventTest": makeGetcwdEventTestData(t), + "GetcwdFailureEventTest": makeGetcwdFailureEventTestData(t), "IoUringSetupEventTest": makeIoUringSetupEventTestData(t), "IoUringSetupFailureTest": makeIoUringSetupFailureTestData(t), "IoUringEnterEventTest": makeIoUringEnterEventTestData(t), @@ -1589,6 +1590,9 @@ func makeGetcwdEventTestData(t *testing.T) (td testData) { if !exitEv.Equals(ep.ExitEv) { t.Errorf("Expected '%v' but got '%v'", exitEv, ep.ExitEv) } + // getcwd args[0] (buf) is an OUTPUT buffer that is only valid at exit, + // so the path is never read at enter. Instead it is resolved at exit + // from /proc/<tid>/cwd, which must equal the process cwd here. if ep.File == nil { t.Fatalf("Expected getcwd to attach a pathname") } @@ -1600,6 +1604,33 @@ func makeGetcwdEventTestData(t *testing.T) (td testData) { return td } +// makeGetcwdFailureEventTestData locks in that a failed getcwd (negative +// errno return) does NOT resolve or attach a cwd path. The exit handler only +// reads /proc/<tid>/cwd when ret > 0 (success returns the path length); on +// error there is nothing to attach. +func makeGetcwdFailureEventTestData(t *testing.T) (td testData) { + enterEv, enterEvBytes := makeEnterNullEvent(t, defaulTime, defaultPid, defaultTid, types.SYS_ENTER_GETCWD) + td.rawTracepoints = append(td.rawTracepoints, enterEvBytes) + + // ERANGE: buffer too small. Raw syscall reports it as a negative errno. + exitEv, exitEvBytes := makeExitRetEvent(t, defaulTime+100, defaultPid, defaultTid, types.SYS_EXIT_GETCWD, -int64(syscall.ERANGE)) + td.rawTracepoints = append(td.rawTracepoints, exitEvBytes) + + td.validates = append(td.validates, func(t *testing.T, _ *eventLoop, ep *event.Pair) { + if !enterEv.Equals(ep.EnterEv) { + t.Errorf("Expected '%v' but got '%v'", enterEv, ep.EnterEv) + } + if !exitEv.Equals(ep.ExitEv) { + t.Errorf("Expected '%v' but got '%v'", exitEv, ep.ExitEv) + } + if ep.File != nil { + t.Errorf("Expected no cwd attached on getcwd failure, got '%v'", ep.File.Name()) + } + }) + + return td +} + func makeIoUringSetupEventTestData(t *testing.T) (td testData) { enterEv, enterEvBytes := makeEnterNullEvent(t, defaulTime, defaultPid, defaultTid, types.SYS_ENTER_IO_URING_SETUP) td.rawTracepoints = append(td.rawTracepoints, enterEvBytes) diff --git a/internal/generate/classify_test.go b/internal/generate/classify_test.go index 06cf1d7..4efdaee 100644 --- a/internal/generate/classify_test.go +++ b/internal/generate/classify_test.go @@ -214,11 +214,38 @@ func TestClassifyNullSyslog(t *testing.T) { } } +// TestClassifyNullGetcwd pins getcwd as KindNull at enter. +// +// getcwd's args[0] is `char *buf`, an OUTPUT buffer: the kernel writes the +// absolute cwd path into it and the contents only become valid AFTER the +// syscall returns (sys_exit). Reading buf at enter would capture an empty or +// garbage string, so getcwd must NOT be classified as a path-input syscall. +// KindNull is the correct enter kind; the cwd is resolved at exit from +// /proc/<tid>/cwd (see eventLoop.handleNullExit). This test locks that in: +// - the enter kind is KindNull (not KindPathname/KindName), and +// - no pathname field is captured from the buffer at enter. func TestClassifyNullGetcwd(t *testing.T) { r := classifyFromData(t, FormatGetcwd) if r.Kind != KindNull { t.Errorf("getcwd: got kind %d, want KindNull", r.Kind) } + if r.Kind == KindPathname || r.Kind == KindName { + t.Errorf("getcwd: enter must not capture output buf as a path, got kind %d", r.Kind) + } + if r.PathnameField != "" { + t.Errorf("getcwd: no enter-time pathname field expected, got %q", r.PathnameField) + } +} + +// TestClassifyByFieldGetcwdBufNotPath is a defense-in-depth lock-in: even if +// the name-only KindNull override for getcwd were removed, the generic +// field-based classifier must not treat `char *buf` as a pathname. Only the +// field names pathname/path/filename/newname are path-like; "buf" is not, so +// classifyByField must report no match for getcwd's output buffer. +func TestClassifyByFieldGetcwdBufNotPath(t *testing.T) { + if r, ok := classifyByField("char *", "buf"); ok { + t.Errorf("getcwd buf: char *buf must not classify as a field kind, got %d", r.Kind) + } } func TestClassifyNullIoUring(t *testing.T) { |
