Skip to content
FreeBSDDeep Dive Published Updated 8 min readViews unavailable

FreeBSD libnv: Validate nvlists, Transfer Descriptors, and Patch Receivers

Build safer FreeBSD libnv protocols by validating nvlist types, tracking descriptor ownership, and checking security fixes in the socket receiver.

FreeBSD’s base-system libnv stores named values in an nvlist_t and can exchange those lists through sockets or kernel interfaces. It is a small, typed message-building API, not a complete RPC protocol: it does not decide which peer is authorized, define your application’s compatibility policy, or make an accepted request safe to execute.

This guide focuses on the userland libnv interface documented by FreeBSD 15.1’s nv(9) manual. It does not describe OpenZFS’s separate nvlist implementation, and it does not treat a packed buffer as a durable file format or a universally stable network protocol. Keep the message schema, peer policy, version boundary, and resource limits explicit in the application around it.

Treat an nvlist as typed input, not a dictionary of trusted values

An nvlist can hold nulls, booleans, unsigned 64-bit numbers, strings, nested nvlists, file descriptors, binary values, and arrays of supported types. A name is associated with a type, so a field named version is not usable as a version number merely because the name exists. Validate both presence and type before calling a typed getter.

This matters because the nvlist_get_*() and nvlist_take_*() routines abort when the requested name is missing, and the list must not be in an error state. A typed getter also assumes its entry has the corresponding type, so validate both presence and type with nvlist_exists_type() first. Do not use a permissive default for fields whose absence should reject a request. Avoid NV_FLAG_NO_UNIQUE for a new protocol unless duplicate names are an intentional, documented part of the schema; otherwise different consumers can make different decisions about repeated security-sensitive fields.

Most nvlist_add_*() functions return void. They record allocation or insertion failures in the list, which the caller can inspect with nvlist_error(). Check that error state before packing, sending, iterating, or using getters. The first error is retained and cannot be cleared, so abandon and destroy a failed list instead of trying to repair it in place.

Here is a local round-trip that checks both the list error state and field types before reading values:

#include <sys/nv.h>

#include <err.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

int
main(void)
{
    nvlist_t *out, *in;
    void *buffer;
    size_t length;
    int error;
    int accepted;

    out = nvlist_create(0);
    if (out == NULL)
        errx(1, "nvlist_create failed");

    nvlist_add_number(out, "protocol_version", 1);
    nvlist_add_string(out, "operation", "status");
    error = nvlist_error(out);
    if (error != 0) {
        nvlist_destroy(out);
        errc(1, error, "build nvlist");
    }

    buffer = nvlist_pack(out, &length);
    nvlist_destroy(out);
    if (buffer == NULL)
        errx(1, "nvlist_pack failed");

    in = nvlist_unpack(buffer, length, 0);
    free(buffer);
    if (in == NULL)
        errx(1, "nvlist_unpack rejected the buffer");

    accepted = nvlist_error(in) == 0 &&
        nvlist_exists_type(in, "protocol_version", NV_TYPE_NUMBER) &&
        nvlist_get_number(in, "protocol_version") == 1 &&
        nvlist_exists_type(in, "operation", NV_TYPE_STRING) &&
        strcmp(nvlist_get_string(in, "operation"), "status") == 0;
    nvlist_destroy(in);

    if (!accepted)
        errx(1, "invalid request schema");
    return (0);
}

The nvlist_exists_type() checks make the following getters safe for these two fields, and C’s left-to-right && evaluation stops before a getter if its type check fails. In an actual service, validate every required field, enforce numeric ranges and string constraints, reject unknown protocol versions, and map an operation name to a fixed allowlist. Never turn an untrusted string into a shell command, pathname authority, ioctl selector, or arbitrary function name.

Keep packing, socket transport, and descriptors distinct

nvlist_pack() allocates a binary buffer; free that buffer with free(). nvlist_unpack() creates a new list from the supplied bytes. The manual documents byte-order conversion, but that is not a promise that every future FreeBSD release will preserve an undocumented application schema. Include a protocol version and test old/new producer-consumer combinations you intend to support.

The flags passed to nvlist_unpack() must match those used to create the packed list. Nested nvlists are an important exception to automatic validation: the unpacker does not validate flags on nested lists, so a consumer that uses nested values should inspect their flags and validate each nested schema deliberately. An unknown or duplicate field must not override an earlier authorization decision merely because a later component selects a different occurrence.

Packing is not the descriptor-transfer API. If an nvlist contains file descriptors, nvlist_pack() returns NULL; the manual restricts descriptor transfer to Unix-domain sockets through nvlist_send() and nvlist_recv(). nvlist_add_descriptor() duplicates the supplied descriptor. nvlist_move_descriptor() instead consumes it, so the caller must not later close or move that same descriptor as if ownership remained unchanged. On receipt, a getter exposes a value owned by the list; nvlist_take_descriptor() removes it and gives the caller responsibility for closing it.

The distinction changes cleanup paths. Decide whether the list or the surrounding code owns each descriptor, close every transferred handle when its work ends, and destroy the nvlist on both success and error. A descriptor is authority, not just another field: validate the peer, expected descriptor count and role, access mode, and any required Capsicum rights before using it. Receiving a descriptor does not prove that the sender was authorized to delegate that resource.

For local IPC, the FreeBSD manual’s nvlist_send()/nvlist_recv() examples use a Unix-domain socket. Authenticate or authorize the connected peer separately, for example with the credential mechanism appropriate to your socket type. Keep a bounded request schema and reject unexpected handles. Do not assume that byte-order portability provides authentication, confidentiality, replay protection, or transport framing for a protocol you invent.

Those schema limits must not be mistaken for a bound on the receiver’s parsing allocations. In the FreeBSD 15.1 nvlist_recv() implementation, the function reads and sanity-checks the header, derives the packed-buffer size from nvlh_size, allocates that buffer, then allocates the descriptor array from nvlh_descriptors before calling nvlist_xunpack(). Header validation rejects invalid magic/flags and size arithmetic overflow, but it does not impose an application-configured maximum message or descriptor count. Checks of field names, types, nesting, or lengths after nvlist_recv() returns are therefore too late to cap those earlier allocations. If a peer is not trusted, enforce message-byte and descriptor-count limits before those allocations with a transport/decoder that provides and honors such limits. If the selected nvlist_recv() path cannot enforce the required pre-allocation bounds, do not expose it to that peer; authorization, process isolation, and resource limits reduce exposure but do not turn post-parse schema checks into a parser-memory bound. Merely switching to nvlist_unpack() is not a proven substitute unless the chosen framing and decoding path independently bounds every allocation dimension.

Use a system interface as a concrete integration test

FreeBSD’s ifnet(9) manual documents SIOCGIFCAPNV and SIOCSIFCAPNV, nvlist-based variants of the interface-capability ioctls. The query side can report EFBIG, clear the buffer pointer, and return the required length when the caller’s buffer is too small. This is a useful reminder to treat a returned size as a sizing result, not as a partially valid nvlist. Retry with a correctly sized buffer, while bounding allocations and handling the possibility that interface state changes between calls.

The same manual says an interface indicates support for these ioctls with IFCAP_NV; a program must not assume every driver implements them. In the returned list, simple capability names are booleans: presence denotes support and the value reports whether the capability is enabled. On the set side, only names present in the submitted list are affected, and simple capability updates must be booleans. Query supported names and their types before constructing a change, preserve unrelated state, and check the ioctl result. A successful nvlist parse says nothing by itself about whether the requested operation was permitted or took effect.

Account for receiver security and release state

Two 2026 FreeBSD security advisories document separate libnv receiver defects. FreeBSD-SA-26:16.libnv covered a stack overflow when the socket descriptor exceeded select(2)’s FD_SETSIZE handling. FreeBSD-SA-26:17.libnv covered a heap overflow while validating a crafted incoming nvlist header with a byte-order flag. The advisories applied to supported FreeBSD versions at announcement time and list corrected branch revisions and release patch levels. They are not fixed by choosing a smaller application field or by calling nvlist_error() after parsing.

Use the current security advisory and release errata for the exact branch you run; do not copy a patch number from a different release train. Verify both kernel and userland update state through the documented FreeBSD release procedure, install the vendor’s supported correction, and reboot when the advisory requires it. A locally built binary linked to a system library is not proof that the installed base system is patched. The advisories concern the FreeBSD base-system libnv implementation, not OpenZFS’s separate nvlist code.

For a service that accepts messages from less-trusted local clients, add negative tests for a missing field, wrong type, unsupported version, repeated field, nested list, oversized application payload, unexpected descriptor, peer-authorization failure, and a closed or interrupted socket. Exercise resource exhaustion and restart behavior in a disposable test system. Preserve errno and the failure stage in diagnostics, but do not dump binary payloads or received secrets into logs. If the application cannot bound or validate the input before acting, do not expose the receiver to that peer.

libnv gives FreeBSD code a useful typed container and IPC mechanism. Safe use still depends on explicit schema checks, correct ownership, endpoint authorization, bounded work, and a currently patched base system. Keep those guarantees in the service contract rather than attributing them to the serialization format.

Related:

Sources:

Comments