FreeBSD sbuf: Bounded Composition, Error Latching, and Drain Boundaries
Use FreeBSD sbufs to compose bounded or extensible strings, latch failures, finish safely, and understand drain and kernel-context limits.
FreeBSD’s sbuf interface gives kernel and user-space code one object for assembling strings while recording allocation and composition errors. Instead of repeatedly calculating offsets into a character array, a caller appends data and checks the result at a well-defined finish point. The useful contract is not “formatting cannot fail”; it is that the sbuf retains failure state so the caller can avoid publishing a partial result.
The interface is FreeBSD-specific. The FreeBSD 15.1 sbuf(9) manual documents both the kernel and user-space variants and their different return conventions. Treat the API and return conventions as target-release details rather than assuming that code using <sys/sbuf.h> is portable to another Unix.
Select who owns the storage
sbuf_new() can initialize a caller-provided struct sbuf and storage array, or allocate either object when the corresponding pointer is NULL. A supplied storage buffer must have at least the declared length, and the manual warns that directly accessing it while the sbuf is using it has undefined results. Do not retain a raw pointer into that storage while continuing to append.
For a fixed upper bound, initialize a fixed-length sbuf and make overflow an ordinary failure path. SBUF_FIXEDLEN prevents growth; an append that exceeds the capacity marks an overflow condition. This is appropriate when the output size is already bounded by a protocol or kernel interface. It is not a reason to truncate silently: check the append and finish results, then reject the whole message if either reports failure.
For variable-size user-space output, sbuf_new_auto() is the short form for a fully dynamic sbuf. Its storage can grow as needed while resources allow, so it is convenient but not a memory limit. Validate untrusted input and impose application-level size bounds before allowing an attacker-controlled record to expand without limit. In kernel code, consider the allocation context: SBUF_NOWAIT asks extension attempts to fail in low-memory conditions in the manner of M_NOWAIT; it does not make an otherwise unsafe call context safe.
The optional SBUF_INCLUDENUL flag changes whether the final NUL byte is included in the reported sbuf length. sbuf_finish() still writes a NUL terminator for a non-draining sbuf either way, so reserve storage for it even when the reported length excludes it. Decide whether the consumer needs a C string or a byte span and keep that length convention explicit.
Make finish the publication boundary
Every append can fail. A small user-space pattern is to stop composing at the first error, finish only after all intended fields were added, and hand data to the next layer only after finish succeeds:
struct sbuf *sb = sbuf_new_auto();
int status = -1;
if (sb == NULL)
return -1;
if (sbuf_printf(sb, "job=%s\n", job_name) < 0)
goto out;
if (sbuf_printf(sb, "state=%s\n", state_name) < 0)
goto out;
if (sbuf_finish(sb) < 0)
goto out;
/* Application-defined consumer; the bytes are used before sbuf_delete(). */
if (send_record(sbuf_data(sb), (size_t)sbuf_len(sb)) != 0)
goto out;
status = 0;
out:
sbuf_delete(sb);
return status;
This is an illustrative user-space skeleton: send_record() and its error policy belong to the application. In user space, sbuf_finish() returns zero on success and -1 with errno set on error. The kernel variant instead returns ENOMEM for an overflow that was not resolved before finishing, or the attached drain’s error code. Keep that distinction at API boundaries; code should not interpret a kernel error number as the user-space -1 convention.
sbuf_finish() is a state transition, not just a flush hint. With no drain callback it NUL-terminates the buffer. It then marks the sbuf finished, so appending or repositioning is disallowed until sbuf_clear() resets it. sbuf_data() only works on a finished sbuf. If a composition failed, do not use a seemingly useful prefix as a successful complete record. sbuf_len() returns -1 when the buffer overflowed, so only convert its result to an unsigned length after success has been established.
Use sbuf_clear() when a reusable object should start a new composition; it invalidates old contents and resets the position. sbuf_setpos() only permits truncation to a position between zero and the current end. sbuf_delete() is required for every successful sbuf_new() initialization and releases storage allocated for the sbuf. These operations define a clear lifecycle: initialize, append, finish, consume, then clear for reuse or delete.
Drains are streaming interfaces with different rules
An attached drain callback can keep a producer from retaining the entire output in memory. FreeBSD invokes it when the internal buffer fills or when sbuf_finish() flushes remaining data. The callback may drain some or all bytes, but it must make progress: returning zero sets EDEADLK; a negative result records an error that is returned by this or a later finish call. A two-byte buffer is the documented way to request unbuffered draining, at the cost of calling the drain for every added byte.
Do not treat a drained sbuf like an in-memory string. The manual says sbuf_data() and sbuf_len() cannot be used with a drain, along with several replace/copy/clear operations. If the caller needs a complete buffer for hashing, retry, or atomic publication, use a non-draining sbuf and check its capacity policy instead. If it needs streaming, make the callback’s partial-write, downstream failure, and retry behavior explicit.
There is also a user-space nuance: sbuf_printf() and sbuf_vprintf() cannot know that a formatted append will overflow until formatting is complete, so libsbuf may extend storage to finish one formatting call even when a drain exists. A drain therefore is not an absolute peak-memory ceiling for a large single formatted append.
The operational checklist is short: choose fixed versus extensible storage deliberately, bound data before formatting, check every append and the final finish, consume only finished non-draining buffers, and release each sbuf exactly once. sbuf centralizes buffer state and reports errors; it does not validate format-string provenance, escape untrusted text for HTML or SQL, or make a partially transmitted record atomic.
Related:
- FreeBSD libxo: Design Stable Structured Output for CLI Automation
- FreeBSD Kernel Memory Triage with vmstat, UMA, and malloc
Sources: