Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,18 @@ changes; the linked API and deployment documentation contains operational detail
databases require the explicit upgrade to schema 26. See
`docs/operations/configuration.md#destination-limits-and-opt-outs`.

### Fixed
- An executor counting packets with eBPF (`enforcement_mode: ebpf`) no longer
fails every run that closes a socket (#412). Closing a counted TCP or UDP
connection, dialed or accepted, deleted its socket-storage entry by the
descriptor it had just closed, so the guest's close failed with `close_tcp:
delete: bad file descriptor` and the run ended "debuglet failed" although
its output was delivered. Close no longer deletes the entry: it belongs to
the socket, and the kernel frees it when the socket is destroyed. Deleting
it before the close instead would let the data a socket still holds after
close leave unattributed and past its rate limit; that data keeps the run's
UUID and is dropped once the run's rate entry is removed.

## [0.3.0-rc.1] - 2026-10-06

### Security
Expand Down
37 changes: 27 additions & 10 deletions internal/executor/ratelimit/ebpf/bpf_conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@
package ebpf

import (
"github.com/netsec-ethz/debuglet/internal/executor/debuglet/socket/netutil"
"net"
"sync"
"time"

"github.com/google/uuid"
"github.com/netsec-ethz/debuglet/internal/executor/debuglet/socket/netutil"
)

type BpfConn struct {
count *BpfCount
conn net.Conn
socketID uint32
domain string
id uuid.UUID
resolvedIPv6 netutil.IPv6

closeOnce sync.Once
closeErr error
}

func (bc *BpfConn) Read(b []byte) (n int, err error) { return bc.conn.Read(b) }
Expand All @@ -29,16 +32,30 @@ func (bc *BpfConn) RemoteAddr() net.Addr { return bc.conn.RemoteAd
func (bc *BpfConn) SetDeadline(t time.Time) error { return bc.conn.SetDeadline(t) }
func (bc *BpfConn) SetReadDeadline(t time.Time) error { return bc.conn.SetReadDeadline(t) }
func (bc *BpfConn) SetWriteDeadline(t time.Time) error { return bc.conn.SetWriteDeadline(t) }

// Close closes the socket and then detaches its rate limit, once. Later calls
// return the first call's result.
//
// Close does not delete the socket's debuglet_sk_map entry. That map is an
// SK_STORAGE map: the entry belongs to the socket and the kernel frees it when
// the socket is destroyed. Until then, data the kernel still sends after
// close(2) (the unsent send buffer, the FIN, retransmissions) keeps this run's
// UUID and is dropped once Detach has removed the rate entry. Deleting the
// entry earlier would let that tail leave unattributed and unthrottled, and
// deleting it after close(2) addresses a descriptor that no longer names the
// socket (#412).
func (bc *BpfConn) Close() error {
bc.closeOnce.Do(func() { bc.closeErr = bc.close() })
return bc.closeErr
}

func (bc *BpfConn) close() error {
// Close connection before detaching/removing ratelimit, otherwise ratelimited writes will
// be passed through before the connection is actually closed
err := bc.conn.Close()
err2 := bc.count.Detach(bc.domain, bc.id, bc.resolvedIPv6)
if err3 := bc.count.objs.DebugletSkMap.Delete(bc.socketID); err3 != nil {
return err3
}
if err2 != nil {
return err2
closeErr := bc.conn.Close()
detachErr := bc.count.Detach(bc.domain, bc.id, bc.resolvedIPv6)
if closeErr != nil {
return closeErr
}
return err
return detachErr
}
Loading