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 <noreply@anthropic.com>
Signed-off-by: Cory Snider <csnider@mirantis.com>
This commit is contained in:
Cory Snider
2026-07-29 13:53:29 -04:00
parent 3ed37aa574
commit 020135c2aa

View File

@@ -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)
}