From 020135c2aab88e98ad31a42e2f69eb572677722c Mon Sep 17 00:00:00 2001 From: Cory Snider Date: Wed, 29 Jul 2026 13:53:29 -0400 Subject: [PATCH] d/libn/i/nftables: free nft_ctx when unreachable The libnftables context is C-allocated memory which is only freed when nftCtx.Close() is called. Attach a runtime cleanup so that a context which is dropped without being closed is freed once it becomes unreachable, instead of being leaked for the lifetime of the process. runtime.AddCleanup needs a pointer to a Go-heap object to attach to, so wrap the libnftables handle in a Go struct rather than type-defining the C struct. Keep the nftCtx reachable across the C calls in Apply(), and across Cleanup.Stop() in Close(): a receiver may otherwise become unreachable at its last mention, and Stop() has no effect once the cleanup has already been queued for execution. Co-Authored-By: Claude Opus 5 Signed-off-by: Cory Snider --- .../internal/nftables/nft_cgo_linux.go | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/daemon/libnetwork/internal/nftables/nft_cgo_linux.go b/daemon/libnetwork/internal/nftables/nft_cgo_linux.go index 02cecccbae..6674eb5855 100644 --- a/daemon/libnetwork/internal/nftables/nft_cgo_linux.go +++ b/daemon/libnetwork/internal/nftables/nft_cgo_linux.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "runtime" "unsafe" "github.com/containerd/log" @@ -24,7 +25,13 @@ func preflight() error { return nil } -type nftCtx C.struct_nft_ctx +// nftCtx owns a libnftables context. The context is freed by [nftCtx.Close], +// or by a runtime cleanup if the nftCtx becomes unreachable without being +// closed. +type nftCtx struct { + handle *C.struct_nft_ctx + cleanup runtime.Cleanup +} // Apply calls libnftables to execute the nftables commands in nftCmd. func (h *nftCtx) Apply(ctx context.Context, nftCmd []byte) error { @@ -34,9 +41,12 @@ func (h *nftCtx) Apply(ctx context.Context, nftCmd []byte) error { cCmd := C.CString(string(nftCmd)) defer C.free(unsafe.Pointer(cCmd)) - ret := C.nft_run_cmd_from_buffer((*C.struct_nft_ctx)(h), cCmd) - stdout := C.GoString(C.nft_ctx_get_output_buffer((*C.struct_nft_ctx)(h))) - stderr := C.GoString(C.nft_ctx_get_error_buffer((*C.struct_nft_ctx)(h))) + ret := C.nft_run_cmd_from_buffer(h.handle, cCmd) + stdout := C.GoString(C.nft_ctx_get_output_buffer(h.handle)) + stderr := C.GoString(C.nft_ctx_get_error_buffer(h.handle)) + // Keep h reachable until libnftables is done with its context, so that the + // cleanup can't free it out from under these calls. + runtime.KeepAlive(h) if ret != 0 { return fmt.Errorf("libnftables: failed to apply commands (code %d), stderr: %s", int(ret), stderr) } @@ -60,9 +70,22 @@ func newNftCtx() (_ *nftCtx, retErr error) { if ret := C.nft_ctx_buffer_error(handle); ret != 0 { return nil, fmt.Errorf("libnftables: failed to set error buffer (code %d)", int(ret)) } - return (*nftCtx)(handle), nil + h := &nftCtx{handle: handle} + h.cleanup = runtime.AddCleanup(h, func(handle *C.struct_nft_ctx) { + C.nft_ctx_free(handle) + }, handle) + return h, nil } +// Close frees the libnftables context. It is idempotent, but h must not be used +// for anything else after it's been closed. func (h *nftCtx) Close() { - C.nft_ctx_free((*C.struct_nft_ctx)(h)) + h.cleanup.Stop() + if h.handle != nil { + C.nft_ctx_free(h.handle) + h.handle = nil + } + // Stop only cancels the cleanup if h hasn't already become unreachable, so h + // must be kept alive across the call to avoid a double free. + runtime.KeepAlive(h) }