summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-30 21:43:40 +0300
committerPaul Buetow <paul@buetow.org>2026-05-30 21:43:40 +0300
commitab2053c6c618ce01d7e18a5e3584cfafc6e58ab4 (patch)
treea354ac9ec66971f2e58631346cb3be17d01d679b /internal
parent6be2f977861bda44d10d5f261e220619353233eb (diff)
test(socketpair): lock in domain-is-not-an-fd invariant (c00)
Audit of socketpair(2) found the tracing implementation already correct: KindSocketpair captures the two output fds from the sv[2] buffer (args[3]) at exit and never treats args[0] (the address-family/domain constant) as a file descriptor. Family=Network and UNCLASSIFIED ret are consistent with the socket/accept siblings and the docs. Add regression lock-in tests so a future field-shape or classification change cannot silently regress to recording the domain integer as a bogus fd: - TestClassifySocketpairNotFd: pins the name-based override so socketpair is KindSocketpair, never the generic KindFd path that reads args[0]. - TestHandleSocketpairExitDoesNotTrackDomainAsFd: uses AF_INET6 (10), distinct from the returned fds, and asserts fd 10 is never tracked while sv0/sv1 are. - TestHandleSocketpairExitDropsFdsOnError: on ret!=0 no descriptors are tracked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/eventloop_socket_test.go93
-rw-r--r--internal/generate/classify_test.go19
2 files changed, 112 insertions, 0 deletions
diff --git a/internal/eventloop_socket_test.go b/internal/eventloop_socket_test.go
index 59e995d..1a098fa 100644
--- a/internal/eventloop_socket_test.go
+++ b/internal/eventloop_socket_test.go
@@ -108,6 +108,99 @@ func TestHandleSocketpairExitTracksReturnedFdsFromExitEvent(t *testing.T) {
verifyFileDescriptor(t, el, 62, "socket:1:1:0")
}
+// TestHandleSocketpairExitDoesNotTrackDomainAsFd is a regression lock-in for the
+// socketpair(2) audit (task c00). socketpair's first argument (args[0]) is the
+// address-family/domain constant (e.g. AF_UNIX, AF_INET6), NOT a file
+// descriptor: the two created fds are written by the kernel into the OUTPUT
+// array sv[2] (args[3]) and are only valid AFTER the call returns. KindSocketpair
+// captures sv0/sv1 from that output buffer at exit; it must never register the
+// domain integer as an fd. This test pins that invariant by using a Family value
+// (AF_INET6 == 10) that is numerically distinct from the returned fds and
+// asserting fd 10 is never tracked.
+func TestHandleSocketpairExitDoesNotTrackDomainAsFd(t *testing.T) {
+ el := mustNewEventLoop(t, eventLoopConfig{})
+
+ const afInet6 = 10
+ enter := &types.SocketpairEvent{
+ EventType: types.ENTER_SOCKETPAIR_EVENT,
+ TraceId: types.SYS_ENTER_SOCKETPAIR,
+ Time: 100,
+ Pid: 77,
+ Tid: 78,
+ Family: afInet6,
+ Type: 1,
+ Protocol: 0,
+ Sv0: -1,
+ Sv1: -1,
+ Ret: 0,
+ }
+ exit := &types.SocketpairEvent{
+ EventType: types.EXIT_SOCKETPAIR_EVENT,
+ TraceId: types.SYS_EXIT_SOCKETPAIR,
+ Time: 200,
+ Pid: 77,
+ Tid: 78,
+ Family: afInet6,
+ Type: 1,
+ Protocol: 0,
+ Sv0: 3,
+ Sv1: 4,
+ Ret: 0,
+ }
+ ep := &event.Pair{EnterEv: enter, ExitEv: exit}
+
+ if ok := el.handleSocketpairExit(ep, enter); !ok {
+ t.Fatal("handleSocketpairExit returned false")
+ }
+ // Only the output fds sv[2] are tracked.
+ verifyFileDescriptor(t, el, 3, "socket:10:1:0")
+ verifyFileDescriptor(t, el, 4, "socket:10:1:0")
+ // The domain constant (AF_INET6 == 10) must NOT have been captured as an fd.
+ verifyFdNotTracked(t, el, afInet6)
+}
+
+// TestHandleSocketpairExitDropsFdsOnError pins that a failed socketpair(2)
+// (ret != 0) tracks no descriptors: the sv[2] output buffer is undefined on
+// error, so the BPF exit handler leaves sv0/sv1 at the -1 sentinel and the
+// userspace handler must not register anything.
+func TestHandleSocketpairExitDropsFdsOnError(t *testing.T) {
+ el := mustNewEventLoop(t, eventLoopConfig{})
+
+ enter := &types.SocketpairEvent{
+ EventType: types.ENTER_SOCKETPAIR_EVENT,
+ TraceId: types.SYS_ENTER_SOCKETPAIR,
+ Time: 100,
+ Pid: 77,
+ Tid: 78,
+ Family: 1,
+ Type: 1,
+ Protocol: 0,
+ Sv0: -1,
+ Sv1: -1,
+ Ret: 0,
+ }
+ exit := &types.SocketpairEvent{
+ EventType: types.EXIT_SOCKETPAIR_EVENT,
+ TraceId: types.SYS_EXIT_SOCKETPAIR,
+ Time: 200,
+ Pid: 77,
+ Tid: 78,
+ Family: 1,
+ Type: 1,
+ Protocol: 0,
+ Sv0: -1,
+ Sv1: -1,
+ Ret: -24, // -EMFILE
+ }
+ ep := &event.Pair{EnterEv: enter, ExitEv: exit}
+
+ if ok := el.handleSocketpairExit(ep, enter); !ok {
+ t.Fatal("handleSocketpairExit returned false")
+ }
+ verifyFdNotTracked(t, el, 1)
+ verifyFdNotTracked(t, el, -1)
+}
+
func TestHandleAcceptExitTracksAcceptedFd(t *testing.T) {
el := mustNewEventLoop(t, eventLoopConfig{})
diff --git a/internal/generate/classify_test.go b/internal/generate/classify_test.go
index 3053615..3be393d 100644
--- a/internal/generate/classify_test.go
+++ b/internal/generate/classify_test.go
@@ -843,6 +843,25 @@ func TestClassifyExitSocketpair(t *testing.T) {
}
}
+// TestClassifySocketpairNotFd is a regression lock-in for the socketpair(2)
+// audit (task c00). socketpair(int domain, int type, int protocol, int sv[2])
+// takes the address-family/domain constant as args[0] (named "family" in the
+// tracepoint format), NOT a file descriptor. The created fds are written into
+// the OUTPUT array sv[2] (args[3]) and are only valid after the call returns.
+// socketpair must therefore be KindSocketpair (read sv[2] at exit), never
+// KindFd, which would record the domain integer as a bogus fd. Pin that the
+// name-based override wins so a future field-shape change cannot make it fall
+// through to the generic KindFd path.
+func TestClassifySocketpairNotFd(t *testing.T) {
+ r := classifyFromData(t, FormatSocketpair)
+ if r.Kind == KindFd {
+ t.Fatal("socketpair classified as KindFd: args[0] is the domain constant, not an fd")
+ }
+ if r.Kind != KindSocketpair {
+ t.Errorf("socketpair: got kind %d, want KindSocketpair", r.Kind)
+ }
+}
+
func TestClassifyPipe(t *testing.T) {
r := classifyFromData(t, FormatPipe)
if r.Kind != KindPipe {