From df373db740383b16050d75544604e596138eb8c8 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Tue, 2 Jun 2026 10:27:19 +0300 Subject: fix(close): deregister fd only on successful close (ret==0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit applyFdCloseState evicted the fd->path mapping (fdState + proc-fd cache) on every close exit, ignoring the return value. A failed close — most importantly EBADF ("fd isn't a valid open file descriptor"), but also EINTR/EIO — did not release any descriptor we track, so dropping the mapping there would let a later genuine close or a reuse of the fd number resolve against stale/empty state. Gate the eviction on ret==0, mirroring the ret==0 guard already used by applyCloseRangeState for close_range. close's exit tracepoint is generated as a ret_event (UNCLASSIFIED), so the exit carries the close return value in RetEvent.Ret. Tests: the close-exit event was being built as an fd_event, which does not match the real BPF wire format (sys_exit_close emits EXIT_RET_EVENT). Add a makeExitCloseEvent helper that emits the correct ret_event, route all 18 close-exit call sites and TestHandleFdExitCloseClearsProcFdCache through it, and add CloseFailureTest asserting a failed close (ret=-1) leaves the fd tracked. Co-Authored-By: Claude Opus 4.8 --- internal/eventloop_exit.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) (limited to 'internal/eventloop_exit.go') diff --git a/internal/eventloop_exit.go b/internal/eventloop_exit.go index c1465fc..ae085c6 100644 --- a/internal/eventloop_exit.go +++ b/internal/eventloop_exit.go @@ -143,12 +143,23 @@ func (e *eventLoop) handleFdExit(ep *event.Pair, fdEv *types.FdEvent) bool { return true } -// applyFdCloseState updates fd-tracking state for the close syscall. +// applyFdCloseState updates fd-tracking state for the close syscall. The fd is +// deregistered only on a SUCCESSFUL close (ret == 0): per close(2), a failed +// close — most importantly EBADF, "fd isn't a valid open file descriptor" — +// did not release any descriptor we are tracking, so evicting the mapping there +// would drop a still-valid fd->path entry and let a later genuine close (or a +// reuse of the number) resolve against a stale/empty state. This mirrors the +// ret == 0 gate already applied by applyCloseRangeState for close_range. func (e *eventLoop) applyFdCloseState(ep *event.Pair, fd int32, pid uint32) { - if ep.Is(types.SYS_ENTER_CLOSE) { - e.fdState().delete(fd) - e.fdState().deleteProcFdCache(fd, pid) + if !ep.Is(types.SYS_ENTER_CLOSE) { + return + } + retEv, ok := ep.ExitEv.(*types.RetEvent) + if !ok || retEv.Ret != 0 { + return } + e.fdState().delete(fd) + e.fdState().deleteProcFdCache(fd, pid) } // applyFdTransferOp handles dup/dup2 and pidfd_getfd fd-transfer operations. -- cgit v1.2.3