Skip to main content

Part 3: Buffers, Padding & Shift Buffers

If your change frames payloads, prepends a header, rewrites bytes in place, or allocates working memory, you must understand sbuf_t, the buffer pools, and the chain-wide left-padding contract. Getting this wrong produces tunnels that work in one chain layout and corrupt memory in another.

Source of truth: ww/bufio/shiftbuffer.h, ww/bufio/buffer_pool.h, and the required_padding_left field in ww/objects/node.h.

The sbuf_t Anatomy​

WaterWall payloads are carried in sbuf_t ("shift buffer"), a padded, shiftable byte buffer. Its header is exactly 32 bytes and the allocation's data area is 32-byte aligned:

struct sbuf_s
{
uint32_t curpos; // offset of the current payload start within buf[]
uint32_t len; // current payload length, in bytes
uint32_t capacity; // ordinary: storage capacity; splice: logical geometry
uint16_t l_pad; // reserved left padding (constant once created)
uint16_t flags;
uint8_t buf[]; // 32-byte aligned data
};

For ordinary buffers, the cursor moves inside a fixed allocation. The flag kSbufFlagSplice gives the payload a different representation, described below.

Fresh and reset ordinary buffers start at padding rounded to 32 bytes. Moving curpos can change the current payload's alignment; allocation alignment alone does not justify aligned loads or stores through sbufGetRawPtr() or sbufGetMutablePtr(). Check the actual address, including any copy offset.

The storage diagram and accessor descriptions here apply to ordinary buffers:

   |<-- left capacity -->|<------- payload (len) ------->|<-- writeable tail -->|
+---------------------+-------------------------------+----------------------+
buf[0] curpos curpos+len capacity
|<------ l_pad ------>|
(reserved padding)
  • curpos moves left when you prepend (shift left) and right when you consume from the front.
  • len is how many payload bytes are valid starting at curpos.
  • Left capacity (sbufGetLeftCapacity = curpos) is how many bytes you can still prepend by shifting left.
  • Writable capacity (sbufGetMaximumWriteableSize = capacity - curpos) is how much you can address forward from the cursor, including current payload.
  • Tail capacity (sbufGetTailCapacity = capacity - curpos - len) is the spare room available to append bytes without growing an ordinary buffer. It requires valid ordinary geometry; it is not available for splice buffers.

Key Accessors​

CallReturns
sbufGetLength(b)current payload length (len)
sbufGetRawPtr(b) / sbufGetMutablePtr(b)pointer to the payload start (buf + curpos)
sbufGetLeftCapacity(b)bytes available to prepend (curpos)
sbufGetMaximumWriteableSize(b)bytes addressable from the cursor (capacity - curpos)
sbufGetTailCapacity(b)spare append space in an ordinary buffer (capacity - curpos - len)
sbufGetTotalCapacity(b)full allocation (constant)
sbufGetLeftPadding(b)the original aligned l_pad value

Reading and Writing​

Aligned and unaligned integer helpers exist for header fields:

sbufWriteUI16(b, port);            // aligned 16-bit write at the cursor
sbufWriteUnAlignedUI32(b, value); // unaligned 32-bit write
uint16_t v; sbufReadUI16(b, &v); // aligned read
sbufWrite(b, src, n); // raw copy of n bytes at the cursor
sbufConsume(b, n); // drop n bytes from the payload tail count
sbufShiftRight(b, n); // advance cursor: consume n bytes from the front

sbufSlice(source, bytes) consumes leading ordinary bytes into a new ordinary buffer with the source's full original aligned left padding. The caller owns both the result and the remaining source, including when the source becomes empty.

Packet checksum repair requests​

kSbufFlagRecalculateChecksum uses the existing sbuf_t.flags storage without changing the 32-byte header. sbufSetRecalculateChecksum(buf, requested) changes only this bit; sbufGetRecalculateChecksum(buf) reads it. Requesting repair requires an ordinary resident buffer and asserts against a splice wrapper.

True means this packet has an outstanding request for the existing checksum repair operation. False means no repair requested; it does not certify checksum validity. Deliberately invalid checksums remain possible. The bit is independent of TUN-private checksum assurance, hardware offload and GSO metadata.

Fresh allocations, reset and ordinary pool reuse clear the request. Whole-payload sbufDuplicateTo(), sbufDuplicate() and sbufDuplicateByPool() copy it exactly, including clearing a previously marked destination when the source is unmarked. A failed duplicate leaves destination bytes, geometry and flags unchanged. Each clone owns its own request; consuming one does not change another. sbufReserveSpace() preserves the request across replacement or no growth; use only its returned pointer after it may replace the allocation.

Byte-copy helpers, including sbufWriteBuf(), do not transfer intent. New partial moves, slices and range extractions start unmarked and do not consume the source's request. Concatenation preserves the destination bit through growth without ORing source bits. Cursor/length changes preserve the bit mechanically; mutable access and byte writers do not infer checksum consequences. Packet-aware callers must complete repair before losing a packet boundary, or explicitly assign intent to complete constructed packets. Segmentation, fragmentation and synthetic output choose intent for each result according to their checksum generation semantics. Splice wrappers stay unmarked; materialization never creates packet intent.

Forwarding transfers bytes and intent together. After forwarding or recycling, the sender must not inspect or change either. A successful complete repair clears the bit; an incremental correction covering only a transform's own edit preserves an earlier request. A terminal consumer may take it before repair because refusal discards the packet. A transform that declines and forwards unchanged preserves both bytes and request. See packet boundaries in Part 4.

Splice payloads: kSbufFlagSplice​

A splice allocation keeps the common 32-byte sbuf_t header and exactly 32 bytes of control storage plus reserved left padding. Metadata at buf + l_pad contains its int pipefd[2] and actual kernel capacity. A complete sbuf_t * is itself the master-pool entry: there is no separate pipe record or entry pointer. kSbufFlagSplicePooled identifies fixed inventory entries and remains set while cached or awaiting release after retirement. No source socket or WIO is retained.

sbufSplicePoolInitialize(capacity_limit, pipe_capacity, max_pipe_count, padding) runs during exclusive startup, after every configuration's chains have finalized padding and before any prepare/start hook or producer runs. It freezes the aligned padding and configures empty master storage and the capacity/count ceilings. It returns whether configuration succeeded and allocates no buffers or pipes. Checkout reuses existing entries first, then creates only the missing requested batch under a shared growth mutex. Only nonblocking, close-on-exec pipes meeting the required capacity and remaining aggregate budget enter the inventory; account their actual kernel-reported capacity. A pipe creation or sizing refusal stops that refill, retains earlier successes and delays further creation attempts for one second across all workers. Ordinary-buffer fallback remains available. Heap allocation follows the fatal policy.

Production requests 1 MiB pipes. The process-wide capacity ceiling follows the RAM profile:

ProfileCapacity ceiling
S1 (minimal / ultralow, numeric 0/1)32 MiB
S2 (numeric 2)64 MiB
M1 (client, numeric 3)128 MiB
M2 (client-larger, numeric 4)256 MiB
L1 (numeric 5)384 MiB
L2 (server, numeric 6) and higher internal profiles512 MiB

When a successful initial memory snapshot is available, cap this ceiling at one fifth of effective_total_bytes: host RAM bounded by every applicable visible finite cgroup limit. Use total capacity, never free/available RAM, and round down to whole 1 MiB pipes. The resulting ceiling may be below 32 MiB or zero. If memory detection is unsupported or unavailable, retain the profile ceiling and emit a diagnostic. Freeze the budget immediately after global-state construction; its initial total remains usable even if startup has outlasted the sampler's freshness window. Do not recalculate it from runtime pressure samples. Empty pipe capacity does not reserve all corresponding payload memory.

The shared inventory owns one master_pool_t with room for every permitted entry, so cache overflow cannot close a healthy pipe. Each worker buffer pool caches at most eight complete empty splice buffers, independently of its ordinary cache width. A local miss takes up to four existing entries from the master or bounded lazy growth; a partial batch is valid. masterpoolTryGetItems() itself never allocates missing entries. On local cache overflow, four entries return to the master before retaining the new return. These limits count unused cached buffers, not active buffers held by a worker. Local plus master plus active entries never exceed the admitted inventory. Healthy entries remain allocated until final teardown. Lifetime admitted bytes/count enforce the growth limits separately from current usable bytes/count, so retiring a failed pipe does not permit a replacement.

If both the requesting worker's cache and the master are empty and bounded growth cannot supply an entry, checkout returns NULL with ENOBUFS, even when another worker retains unused cached entries. Unsupported builds use ENOSYS. The caller chooses whether to defer reading or use ordinary storage; existing I/O paths use ordinary fallback where possible. There is no cross-worker cache stealing or waiting for a return.

Every pooled buffer retains its original aligned l_pad. Inventory padding is shared through GSTATE.splice_inventory. Allocation, recycling, direct destruction and padding updates reject a mismatch with a fatal diagnostic in every build, before interpreting metadata using an altered offset. curpos may still move within advertised headroom. sbufCreateSplice(pad_left) only allocates an unmanaged empty representation; it cannot acquire or create an inventory pipe. Its separate geometry is useful for explicit representation fixtures, not inventory growth.

kSbufFlagSplice identifies the splice representation and stays set while cached. Use the inline sbufIsSplice(buf) helper to query it on a valid buffer; it does not inspect payload bytes or require a pipe syscall. Any claimed body bytes must already be in the wrapper's private pipe. There is no separate body-location flag. An empty body may have an uninitialized pipe or an existing empty pair; empty-body helpers need no pipe creation. The real prefix occupies reserved headroom:

prefix_bytes = l_pad - curpos
body_bytes = len - prefix_bytes

Length and capacity describe logical payload, not resident bytes. Prefix consumption advances the cursor; body consumption reduces length and logical capacity without advancing into metadata. Prepending within advertised padding is permitted. Inspecting or overwriting the kernel-backed body or control storage through generic byte accessors is not permitted; those accessors do not add splice checks. Opaque synchronous forwarding and size accounting remain valid.

sbufGetResidentPrefixLength() returns the addressable prefix of a splice buffer, or the full length of an ordinary buffer. After successful pipe I/O, sbufSpliceConsumeBody(buf, bytes) updates length and logical capacity together without moving the cursor. This accounting helper does not read or discard pipe bytes itself; use it only for bytes already removed from the private pipe.

sbufEnsureOrdinary(pool, buf) returns ordinary input unchanged. For exclusively owned splice input it materializes the complete resident prefix and private-pipe body into best-fit ordinary storage with the pool's large-buffer padding, then recycles the source wrapper. The caller owns only the returned buffer. No callbacks occur, and allocation follows bufferpoolGetBestFit()'s fail-fast contract. The checked counterpart, sbufTryEnsureOrdinary(pool, buf), returns NULL when the destination geometry cannot be represented, leaving the source unchanged and caller-owned. Allocation otherwise retains the pool's fail-fast behavior. HalfDuplexServer uses this checked helper so refusal still closes its association without consuming the rejected input. The node chooses when ordinary storage is needed; custom framing, range extraction, and pre-conversion retained-charge checks keep their specialized conversion paths.

Mode selection and pipe-backed reads​

The core misc.splice boolean defaults to true. Setting it to false keeps chain->supports_splice off for every chain. Finalization evaluates this startup policy after topology expansion, alongside platform support, absence of packet nodes, and splice support from every node. Enabling the option does not override those eligibility checks or change node capability flags.

When splice is enabled on a supported Linux build, core startup first tries to raise fs.pipe-user-pages-soft to the smaller of the calculated capacity ceiling converted to system pages and a finite fs.pipe-user-pages-hard. An unlimited hard limit (0) does not reduce the target. Page conversion rounds up if needed. These live system-wide settings govern each user's aggregate pipe-page allowance; they are not per-process resource limits. An unlimited soft limit (0), or a soft value already at or above the target, causes no change. Read/parse/write and page-size-query failures are nonfatal. The hard limit and fs.pipe-max-size are never changed, and no persistent sysctl configuration is written. A successful increase remains in effect after WaterWall exits.

Core constructs global state and tunes pipe limits. sbufSplicePoolPrepare() publishes the small shared control object before imports can copy global state, without allocating buffers or pipes. Its heap allocation follows the fatal policy; unsupported builds have no inventory. Core then builds all configurations with nodemanagerBuildConfigFile(). Chain finalization accumulates the maximum padding across configurations. The core configures the empty inventory with that padding, then nodemanagerStartConfigs() admits packet bootstrap, prepares all configurations, and starts producers. GSTATE.splice_inventory owns the master, capacity/count ceilings, live count/capacity, growth synchronization, immutable padding and optional discard descriptor. State copies share this object even when taken before final padding is known. Final inventory configuration fills that same object; it never replaces it. Padding is unset during exclusive configuration and frozen before any checkout. State copies do not copy counters or create another inventory. In addition to the capacity budget, the pipe count is capped at one eighth of the remaining descriptor allowance: subtract current open descriptors from the process's soft RLIMIT_NOFILE, then divide by eight. Its two descriptors per pipe therefore use at most one quarter of the allowance still available at startup. The Linux descriptor scan includes its own temporary descriptor conservatively; an unavailable limit or scan leaves the inventory empty. Kernel resource limits may reduce successful acquisition further. Startup reports the configured capacity ceiling and zero allocation; sbufSplicePoolCapacityLimit() exposes the immutable byte ceiling. sbufSplicePoolCount() and sbufSplicePoolCapacity() expose current usable inventory, including pipes leased to buffers. Retiring a failed pipe reduces these values; no replacement is created. Disabled splice skips both the sysctl operation and inventory allocation.

wioEnableSplice() and wioDisableSplice() select the read mode on the WIO's owning worker. The WIO must be open. Selection may change before or after reads start, including from a read callback; no read stop/restart is needed. The next read observes the selected mode. Already delivered buffers, retained private pipes, and queued writes keep their representations and ownership. Writes dispatch by the buffer's representation, independently of the read mode. Enabling on supported builds returns zero without creating a pipe; unsupported builds return -1 with ENOSYS. New descriptor adoption resets the mode.

A normal line has two one-way ordinary-read preferences, initially clear:

  • linePreferOrdinaryReadUpstream(l) requests ordinary source reads that produce upstream payload: TcpListener applies it to its accepted socket.
  • linePreferOrdinaryReadDownstream(l) requests ordinary source reads that produce downstream payload: TcpConnector applies it to its connected socket.
  • linePreferOrdinaryReadBoth(l) requests both directions on the same line by calling the two directional helpers.

Call these helpers on the live line's owning worker. Requests may occur during Init or later, are idempotent, and remain set for that exact line's lifetime. There is no clear operation: one node must not undo another node's preference. The linePrefersOrdinaryReadUpstream() and linePrefersOrdinaryReadDownstream() queries expose the flags. A clear flag permits the adapter's normal splice policy; it never overrides chain eligibility or the core setting.

TcpListener initially selects splice only for an eligible finalized chain, then applies its preference after Init and before reading. TcpConnector consults its preference when adopting the socket and again before connected reads start. Both adapters also apply preferences at receive and payload/write callback entry, including queued payload, write completion and read Resume. Applying a preference only disables splice; it never re-enables it. TcpListener owns its normal line; TcpConnector borrows it. These two sockets can share the same line and consult different flags. No WIO pointer or ownership is stored in the line header.

These preferences are advisory, not representation guarantees. The receive callback runs after its input was read: that delivery can still be splice, and a preference requested while processing it may only be noticed on the following receive. Nodes must still handle both representations and must not use the flag as permission to inspect private pipe bytes as ordinary memory. Preferences do not propagate to associated lines: a Mux child cannot disable its shared parent's socket, and Mux need not materialize child payload because of a preference. Choosing ordinary reads for a short parsing phase still applies for the rest of the line; there is no automatic restoration after a handshake.

For NIO TCP splice reads, the read loop checks out a splice wrapper with its private pipe already initialized by the pool and calls splice() directly. Each request uses bufferpoolGetSplicePayloadLimit(), independently of ordinary allocation sizes: 1 MiB in every memory profile. There is no socket FIONREAD query before a successful splice delivery. The socket and pipe are nonblocking, and the transfer happens before invoking the Payload callback.

Production pipe capacity and the read limit are both 1 MiB. A full-sized pipe does not promise a full-sized transfer: pipe slot layout and available input can still produce a shorter result. Initialized pipes continue consuming their per-user capacity allowance and two descriptors while idle in the inventory.

Only the actual positive splice result is published as the body length. Logical capacity becomes left padding plus that count; kSbufFlagSplice stays set. A positive short splice produces a smaller valid buffer; remaining socket bytes are handled by later reads. The requested count is an upper bound, not a promise of a full-size transfer.

EINTR retries the transfer. On EAGAIN or a zero result, the empty wrapper is recycled and the loop attempts one ordinary nonblocking receive through the large-buffer path. TCP urgent data can stop splice at the urgent boundary while recv() can advance and deliver subsequent ordinary bytes. With a peer FIN already received, splice can even return zero at that boundary before those bytes are drained. The ordinary receive confirms EOF rather than treating a zero splice result alone as proof that the stream has no remaining data.

A successful fallback delivers ordinary bytes with the actual received length. Ordinary EAGAIN/EINTR returns to the event loop; ordinary zero closes the stream. Other splice errors recycle the wrapper and follow normal read-error closure. If the inventory cannot reuse or create a pipe within its limits, checkout returns NULL with ENOBUFS; the read loop also falls back to an ordinary receive. None of these fallbacks has consumed socket bytes in the splice attempt. Splice mode stays enabled for later reads.

Ordinary TCP receives, including these fallbacks, take the pool's large buffer directly without a socket FIONREAD query. Linux UDP retains its size hint. Disabled, non-TCP, and unsupported splice paths retain ordinary behavior.

The common NIO event handler checks admission before entering the read path; the read path checks again before delivery. If shutdown suppresses delivery after a successful splice, WIO drains and recycles the still-local wrapper. A missing read callback also releases the undelivered buffer. After invoking the callback, the read path accesses neither the buffer nor the source WIO. The callback may close/free the source immediately.

There is no FD-backed wrapper state, socket reservation, or post-callback source reference check. WIO owns its primary descriptor directly; emitted buffers have no dependency on that descriptor's lifetime.

Independent private bodies​

Socket -> this buffer's private pipe -> Payload callback

Buffer A -> private pipe A: [AAA]
Buffer B -> private pipe B: [BBB]

Every emitted splice body is independent as soon as the callback receives it. Splice-aware code may retain the whole buffer or a partially consumed remainder for later use, including after source WIO reclamation. No preparation call or source reference is needed before deferral. Consume the private body through the dedicated pipe helpers and preserve exclusive buffer ownership.

Converting B first reads BBB and leaves AAA intact, including when A is queued. Final stream delivery must still preserve A-before-B order. Pool access remains worker-affine; an explicitly transferred wrapper may use its receiving worker's pool. BufferQueue preserves these owned entries. The fixed-header splice stream accepts ordinary and splice entries together; BufferStream is ordinary-only. Callers must not scan private-pipe metadata as payload.

Conversion and partial reads​

sbufSpliceMaterializeToBuffer(buf, dest, pool) replaces an ordinary destination payload with the real prefix and body, preserving the source cursor and total length. It checks destination capacity through sbuf accessors; the destination's allocation geometry and left padding remain unchanged. It recycles the consumed wrapper, which callers must not access afterward.

sbufSpliceReadToBuffer(buf, dest, bytes) appends exactly the requested prefix/body bytes. It validates source length, cursor, and destination append space, without growing storage or consuming destination headroom. Zero bytes is a validated no-op. The caller retains both buffers and returns the wrapper through bufferpoolReuseBuffer() when finished, discarding any unconsumed remainder.

Router and SniffRouter establish an ordinary-memory boundary while their route state is undecided: each complete splice delivery is materialized before pending storage or classification, preserving onward pool padding. Their pending buffers remain ordinary and are replayed intact when the selected branch is initialized. After route commitment, both callback directions forward opaque buffers unchanged. Metadata-only Router rules can commit during Init and preserve the first splice delivery. Complete-delivery materialization preserves the existing sniff-window semantics, including classification of complete headers larger than the incomplete-input cutoff.

TLS protocol/SNI sniffing reassembles a fragmented ClientHello in bounded caller scratch without modifying the retained wire bytes. The SNI result borrows either the original single-record payload or that scratch; both must remain alive until the caller consumes the view. Router and SniffRouter use this sniffing path. tlsclienthelloParseRecord() retains its single-record offsets and semantics for callers that inspect or rewrite encoded records.

StreamFragmenter's optional ClientHello inspection consumes only the bounded initial wire prefix through these range helpers, including private-pipe and resident-prefix-plus-pipe input. Its ordinary accumulator retains the original record headers for byte-identical replay on unsupported framing or deadline expiry. The replacement is reserved alongside the accumulator before record headers are rewritten; an already-admitted opaque suffix remains in the normal FIFO budget. Later opaque splice wrappers retain their direct forwarding path.

Its candidate allocation requests start at 4 KiB and grow to at most 393,216 bytes; rewriting adds at most 320 bytes. A pool may supply a larger buffer, whose actual capacity still counts against the budget. Generic fragments come from jobs already admitted under the 8 MiB logical-byte limit. With the pool's validated, aligned padding, these requested sizes have representable allocation geometry on supported targets. Keep budget and timer refusals, but do not add recoverable heap or geometry failures to these bounded allocations. At rewrite commitment, allocating the computed rewrite length guarantees the helper's output capacity; assert that result while keeping the rewrite call outside the assertion and the helper's checked capacity API intact.

Both read helpers require kSbufFlagSplice and read only from the wrapper's private pipe. A nonempty body requires an initialized pair. Positive short reads accumulate until the required byte count is reached, and EINTR retries without losing progress. Invalid flags/bounds, EOF, or any other read error before completion use LOGF() and abortProgramNow(1). This includes EAGAIN: the private pipe must already hold all claimed bytes, so an incomplete body cannot be deferred until more bytes arrive. These exact-size helper reads are distinct from the read loop's socket-to-pipe transfer, whose actual positive result determines delivery length. Unsupported conversion and partial reads abort.

Recycling, discard, and physical destruction​

bufferpoolReuseBuffer(pool, buf) takes exclusive ownership and discards remaining payload. Healthy inventory entries retain both descriptors and return to the local cache or shared master; pool bypass returns directly to the master. Unmanaged wrappers are freed, and failed inventory entries are retired without replacement. WIO cleanup, BufferQueue destruction, lineReuseBuffer() and reuseBuffer() share this behavior. Ordinary cache overflow never controls splice inventory lifetime.

A wrapper claiming zero length must already have an empty pipe. The pool uses sbufSpliceIsReusable(buf) to check kernel emptiness with FIONREAD only when BUFFER_POOL_DEBUG == 1, including when pooling is bypassed. Failure uses LOGF() and abortProgramNow(1) before the pipe becomes available to another buffer. This check is independent of NDEBUG. With pool debugging disabled, recycling trusts the consumption helpers' byte accounting. Resetting sbuf fields never empties a pipe.

sbufSpliceDiscard() requires exclusive ownership and leaves an empty wrapper without freeing it or returning a healthy lease. For nonempty payloads it drops the real prefix and drains the private pipe with nonblocking splice() to /dev/null, retrying EINTR and accepting positive short transfers until EAGAIN confirms emptiness. Startup opens one optional discard descriptor after configuring the empty inventory; it stays open and unchanged through all worker and foreign-thread buffer cleanup and closes at final inventory destruction. If opening the sink fails or a splice fails with an error other than EINTR or EAGAIN, discard falls back to nonblocking reads with bounded scratch storage for that call. The read fallback also retries EINTR and drains until EAGAIN. EOF or a failed read closes both owned ends and retires the inventory entry permanently. A later checkout may borrow another remaining pipe but must never recreate the retired one.

sbufDestroySplice() drains and returns a healthy complete inventory buffer to the global master without accessing a worker-local pool. It physically frees retired entries and unmanaged wrappers. This is also sbufDestroy()'s splice path, so foreign-thread cancellation safely returns scarce resources. There is a separate physical destructor for final inventory destruction; it cannot recursively recycle entries into the master being destroyed.

Local caches keep their splice flags and pipe metadata. Their entries count as checked out from the master until returned to it. Worker/device pool destruction flushes them into the global master. After workers, callbacks, active buffers and local caches have settled, sbufSplicePoolDestroy() requires zero outstanding entries, closes remaining pipes, destroys the master and discard descriptor, and frees and clears the owning global-state pointer. Imported state copies borrow the same inventory and must finish before owner destruction. Outstanding entries are a fatal lifecycle invariant violation.

Writing splice payloads​

Supported NIO TCP writes accept splice buffers whose body bytes are already in their private pipes. Empty and prefix-only wrappers need no initialized pipe. Writing or queueing never reads the source socket. Real prefixes use send before bodies use nonblocking splice to the destination. When supported, TCP prefix sends use MSG_MORE when that same buffer has a nonempty private-pipe body, including partial-prefix retries. Ordinary and prefix-only sends also use MSG_MORE when nonempty bytes follow in the same socket's WIO queue; body splices then use SPLICE_F_MORE alongside SPLICE_F_NONBLOCK. Each queued attempt recomputes whether bytes remain beyond the current buffer, so the last send/body clears the hint. The hint permits TCP to coalesce adjacent writes. They remain separate system calls; the hint does not guarantee packet boundaries or immediate transmission if a following write stalls. UDP has its separate datagram contract.

wioWriteWithHint() lets a caller advertise nonempty bytes following in its current drain to the same TCP socket. TcpListener and TcpConnector derive this from their pause queue's remaining byte count after popping the current entry, both on resumed drains and Finish flushes. Empty entries do not count. This hint applies only to the immediate write attempt; it is never stored on a queued buffer. After a partial write or yield, WIO retries use only their own actual queue, which may split a batch but cannot retain a stale caller hint. Ordinary wioWrite() supplies no caller hint. Unsupported backends ignore it.

Actual combined progress drives logical queue accounting. Short sends, EAGAIN, and EINTR retain the remainder in the existing WIO FIFO, shared with ordinary writes. Queued buffers require no source handle and remain valid after source reclamation.

Completion recycles empty wrappers. Rejection, output error, overflow, timeout, forced close, and teardown discard private bodies through the same cleanup path. Graceful TCP close drains the queue normally. Supported NIO UDP destinations instead use the one-datagram path below. Generic wioWrite() rejects splice input fatally on unsupported builds or destinations other than TCP/UDP; wioWriteDatagram() consumes raw-IP splice input and returns -1 with EINVAL. The shared udpSendBuffer() returns ENOSYS for splice input on unsupported builds, which never enable splice chains. Callback admission and reentrancy follow the normal WIO contracts.

UDP datagram sending​

udpSendBuffer() in the event layer retains caller ownership of the buffer on all results, consumes accepted splice prefixes and actual pipe bytes, and returns whole-datagram success, captured error and a separate socket-retirement verdict. NIO and UdpStatelessSocket share it. UDP receives remain ordinary; raw-IP writes never gain splice support. The caller runs the complete transaction and any retirement on the socket's owning worker; the helper does not transfer ownership. The caller supplies that socket owner's buffer pool. Any materialization fallback uses best-fit storage with zero required left padding and returns the temporary buffer to the same pool on success or failure. The input remains caller-owned, including when it was posted from another worker. All sends on that socket, including ordinary sends, remain serialized across the transaction. No callback or other send may run while assembly is pending.

One sbuf is one datagram. Prefix-only and empty splice inputs use one ordinary send. For nonempty bodies, send the real prefix (possibly empty) to the immutable explicit destination with MSG_MORE, transfer the entire body with SPLICE_F_NONBLOCK | SPLICE_F_MORE, then issue an empty send without MSG_MORE to commit. These empty operations do not publish extra datagrams. Do not change a shared socket's connection or default peer. IPv4 and mapped destinations allow at most 65,507 payload bytes; IPv6 allows 65,527, including resident prefixes. Route/MTU errors still belong to the kernel. There is no splitting or UDP retry queue; ordinary sends retain their one-shot policy (UdpStatelessSocket retains its ordinary/priming EINTR retry).

Older Linux UDP sendpage implementations have a finite skb fragment limit, so a small logical body can still be unrepresentable when spread over many pipe slots. A pipe-capacity bound of at most 64 KiB limits its slot count conservatively; resident prefix pages reduce that budget, rounded down to a power-of-two number of pipe slots. For larger or unknown-capacity pipes, select full materialization and one ordinary send before touching the socket. Runtime UDP sends never allocate or resize a probe pipe. This conservative layout fallback preserves datagram atomicity without exceeding the fixed inventory. Pipes whose capacity proves the fragment bound may splice their bodies without userspace reads. Kernel-internal copies are still possible, and no measured performance improvement is promised. The fragment bound follows Linux's skb fragment limit and UDP sendpage assembly; newer kernels use splice-to-socket.

Before priming, normal errors/drop policy settles the untouched input. After successful priming, any short/failed splice or failed final commit retires the socket through its owner. A positive short count may conceal an error that already discarded the pending datagram, so never loop over it or send its suffix. Account actual pipe consumption before recycling. Never clear UDP_CORK or connect to AF_UNSPEC as an abort trick. The shared helper closes no FD itself; its caller must prevent further writes and retire through WIO after settling the buffer, before returning. Write/close callbacks may destroy associated lines. See Part 7 for socket-owned peer cleanup.

BufferQueue splice ownership​

BufferQueue accepts ordinary and splice buffers through its existing push, pop, peek, reserve, and destroy API. It preserves entries, including empty entries, and FIFO order. Logical byte accounting includes each splice buffer's real prefix and remaining private-pipe body; it does not count control-storage capacity.

bufferqueueGetBufLen() reports logical bytes. bufferqueueGetCharge() reports the sum of sbufGetQueueCharge() for entries still owned by the queue. Both are cached O(1) totals, maintained by successful front/back insertion and pop. Empty entries contribute charge even though they add no logical bytes. Callers choose limits, their accounting scope, Pause/Resume decisions and overflow handling. An optional attached buffer_budget_t enforces admission across one or more queues and active reservations. Unbound queues retain their existing accounting without a finite admission policy; parser and Mux policies remain with their owners.

Splice insertion retains the original wrapper and private pipe in every build. It neither copies metadata as payload nor converts or consumes the body. Splice entries must already own any claimed body bytes in their private pipes; empty and prefix-only wrappers need no initialized pipe. Ordinary Debug insertion continues to replace allocations to detect stale aliases.

Successful insertion transfers ownership to the queue. Failed try-insertion leaves the pointer, buffer, private-pipe contents, queue entries, and byte/charge totals unchanged. Unrepresentable totals are refused before ownership transfers, including before ordinary Debug replacement. Heap exhaustion while reserving deque storage is fatal; it does not produce a try-insertion refusal. Pop transfers ownership back to the caller. Peek is borrowed: do not consume or modify a queued buffer through an alias. Pop first, consume through the splice helpers, and reinsert any remainder so queue accounting stays correct.

Use bufferqueuePushBack() or bufferqueuePushFront() when the caller already proves that bytes, queue charge, entry storage and any attached budget admit the buffer. A payload-length limit alone does not prove this: retained capacity can be much larger. Keep the try form wherever those checks can still refuse.

Destroying a nonempty queue discards remaining private bodies and recycles the wrappers on the owning event worker, preserving healthy empty pipes. Drain errors close the affected pair before recycling; teardown never creates a replacement. Destroy releases only this queue's contribution to its attached budget and leaves a valid empty, unbound queue with both totals zero. Empty queue destruction requires no worker context. TCP adapter pause queues use these same ownership rules; final stream delivery order remains the caller's responsibility.

A budget tracks logical bytes, canonical queue-capacity charge and entries in O(1). Initialize all three limits explicitly: SIZE_MAX is unrestricted and zero is a real zero allowance. Admission checks representability even in unrestricted dimensions. Empty buffers consume charge and an entry. Budgets own no buffers, pools, callbacks or flow-control state; each lives at a stable address on its owning worker until all attached queues and active reservations have settled. Limits remain fixed while in use.

bufferqueueTryAttachBudget() requires an unbound queue and atomically admits its existing totals before publishing the binding. It can attach populated parser storage when that queue becomes output storage. Refusal preserves the queue and budget unchanged. Initialize the final line-state object before attaching queues to embedded budgets; copying a temporary with self-references is invalid.

bufferqueueTryRebindBudget() transfers a bound queue's current bytes, charge and entries to another live worker-owned budget. Destination refusal changes neither budget nor the queue; rebinding to the same budget succeeds without re-admission. Success changes no buffers, FIFO positions or representations and performs no allocation or callbacks. Previously popped reservations keep their original budget. Empty queues change their binding too: even empty destruction accesses the bound budget, so its owner must remain alive until rebinding or queue destruction.

Ordinary bufferqueuePopFront() releases the entry's budget cost before returning ownership. When the same scope continues retaining a popped buffer, use bufferqueuePopFrontReserved() with an empty reservation. It moves accounting to the active owner without reducing aggregate usage or attempting new admission. Reduce reservation bytes as plaintext or WIO progress is consumed, before reentrant callbacks can admit more input. Retained capacity and the entry remain charged until responsibility ends. Release clears the reservation; empty release is safe. Never copy one live reservation into two independently released owners.

Same-budget bufferqueueTryPushFrontReserved() and bufferqueueTryPushBackReserved() reserve destination storage before transferring accounting, without charging twice. Their reservation must match the actual buffer cost; reconcile any consumption first. Failure preserves the buffer and reservation; success clears the reservation. bufferqueueReserveExtra() reserves deque slots only, never budget allowance.

Whole-queue moves carry the binding and both cached totals by assignment, then initialize the relinquished source empty and unbound. Destroy an empty destination's storage before overwriting it. Never reset a populated queue to erase accounting. Destroy all queues and release all active reservations before asserting an embedded budget empty and clearing its owner. Recreated queues must attach explicitly again.

BufferStream accepts ordinary entries only. The fixed-header splice stream owns mixed representations for Mux; NIO's internal WIO FIFO stores owned wrappers directly.

Exact extraction from an active head and queue​

bufferRangeTakeExact(pool, &active_head, &tail, bytes) in ww/bufio/buffer_queue_range.h consumes an exact FIFO range from a separately owned mutable head followed by an unbound BufferQueue. The caller must establish complete availability and representable ordinary output geometry, including full pool large-buffer padding, before calling. The active head must be exclusively owned and absent from every queue. The queue must remain unbound: this operation does not preserve the active reservations required by a budgeted parser.

The returned buffer is exclusively caller-owned and has the pool's full large-buffer left padding available. A suitable exact whole head transfers directly; otherwise the helper assembles the requested range. Representation selection follows the contributing source wrappers: a splice wrapper selects splice assembly even when the requested bytes lie only in its resident prefix. Pipe checkout or transfer pressure can instead produce complete ordinary output. The helper retains the authoritative replacement destination from every sbufMoveRangeTo() call, including fallback after partial progress.

Queued buffers are inspected without mutation and popped before consumption. Exhausted heads are recycled through the supplied pool; any suffix remains in the active head before the untouched queued tail. A zero-byte request returns one empty ordinary buffer without consuming or changing either input storage location. The operation runs on the pool's owning worker, invokes no callbacks, and owns no persistent cursor or cleanup object.

Protocol metadata, retained-byte and admission policy, authentication, readiness, reentry guards and lifecycle remain with the caller. Commit protocol progress before delivering the extracted buffer through a callback, and preserve the existing line-reference and association checks around that delivery. Node cleanup continues to release the active head and queue.

Prepending a Header: sbufShiftLeft​

The whole reason l_pad exists is so a tunnel can prepend a protocol header without reallocating or memmoving the payload. You move the cursor left and write into the freed space:

// Reserve room and move the cursor back by header_len bytes.
assert(sbufGetLeftCapacity(buf) >= header_len);
sbufShiftLeft(buf, header_len); // curpos -= header_len; len += header_len

// Now write the header at the new front.
sbufWriteUI16(buf, htons(payload_len));
// ... fill the rest of the header ...

sbufShiftLeft asserts that enough left capacity exists:

static inline void sbufShiftLeft(sbuf_t *const b, const uint32_t bytes)
{
assert(sbufGetLeftCapacity(b) >= bytes);
b->curpos -= bytes;
b->len += bytes;
}

If the assertion fires (or, in a release build, if you shift left past the start), you have prepended more than your padding budget allows — which is the next section.

The Padding Contract: required_padding_left​

A tunnel may only rely on left padding it advertised in its node.c:

.required_padding_left = kMyProtocolHeaderSize,

During chain finalization, the runtime sums required_padding_left across every node in the chain and reserves that much left padding in the buffers it hands out (tunnelchainInsert accumulates sum_padding_left; tunnelchainFinalize calls globalstateUpdateAllocationPadding). In other words:

The left capacity available to your tunnel at runtime is the sum of the padding budgets of your tunnel and every tunnel closer to the adapter that produced the buffer. If you prepend bytes you never advertised, you are spending someone else's budget, and the buffer may not have it in a different chain.

This is why a framing tunnel that "works on my machine" can corrupt memory once a user puts a different node in front of it. Advertise what you prepend.

Rules:

  • Use sbufShiftLeft() only when sbufGetLeftCapacity() is at least the header size you need.
  • Keep every prepend within this tunnel's advertised required_padding_left.
  • If you must prepend more than your budget, either raise required_padding_left to match, or build a fresh buffer (see below) instead of shifting.

Buffer Pools​

For ordinary runtime buffers, do not malloc. Use the worker-local buffer pool so allocations are recycled and correctly padded. The pool is reachable from any line:

buffer_pool_t *pool = lineGetBufferPool(line);     // this line's worker pool

sbuf_t *big = bufferpoolGetLargeBuffer(pool); // ordinary read/working buffer
sbuf_t *medium = bufferpoolGetMediumBuffer(pool); // 32 KiB in S1/S2, 64 KiB in higher profiles
sbuf_t *small = bufferpoolGetSmallBuffer(pool); // small working buffer
sbuf_t *splice_buf = bufferpoolGetSpliceBuffer(pool); // leased private pipe, or NULL when exhausted

// ... use the buffer ...

bufferpoolReuseBuffer(pool, buf); // return it to the pool
lineReuseBuffer(line, buf); // convenience: pool of line's worker

Pools are per worker. A line belongs to exactly one worker (lineGetWID), so always use that line's pool — never a different worker's. Buffers from a pool already carry the chain's reserved left padding, so a pooled buffer is the correct place to build a payload you intend to prepend onto.

Pool metadata and ordinary buffer allocation follow the fatal heap-allocation policy. bufferpoolCreate() remains nullable for invalid construction inputs or unrepresentable geometry. Bounded splice-inventory exhaustion is separate from heap exhaustion and keeps its documented ordinary-buffer fallback.

Each buffer pool has large, medium and small ordinary caches backed by three shared ordinary masters. bufferpoolCreate() takes those masters in that order, then the ordinary cache width, large/medium/small payload capacities, the splice payload limit and the waiting-budget basis. The latter two values must be in [1, INT_MAX] and stay fixed after construction. Device pools inherit the sizes and limits from their worker pool. Splice caches use the shared bounded inventory without a separate wrapper master or a constructor argument for one.

Device backends use devicePoolCreate() with an explicit, exclusively accessible worker pool and a backend-specific minimum small tier. Construction preserves zero initial padding; devicePoolUpdatePadding() copies all four finalized padding values during BringUp, before publishing I/O producers. Neither helper obtains a worker pool from thread-local identity. Private TUN reader pools use the packet MTU as their minimum; Linux worker readers use their owner worker pools and a shared fragment table with detached allocations. Darwin receives its family prefix into separate storage. The worker GSO output allowance and its exact-sized fallback remain a separate calculation.

Worker and device pools select ordinary cache width with PROPER_BUFFER_POOL_WIDTH(). Ordinary masters use twice that width as their constructor argument. Splice caches have a fixed eight-entry limit; their global master is sized for the maximum permitted inventory; the configured profile and detected RAM ceiling affect its global budget, not local cache width.

The master's mutex transfers ownership and visibility of cached item slots. Its atomic len uses relaxed loads/stores: reads outside the mutex are advisory empty/full probes, and slot access always holds the mutex. checked_out has a different role: terminal returns can finish outside that mutex, so their release decrements pair with acquire reads before teardown frees pool-family resources. Observing zero does not close checkout admission; the owner must prevent new checkouts and satisfy the existing lifecycle requirements before destruction.

Master constructors double their argument, giving these actual entry capacities:

ProfileOrdinary local widthEach ordinary master
S1 / minimal14
S2416
M1 / client832
M2 / client-larger32128
L164256
L2 / server128512

Ordinary local pointer arrays hold twice their selected width. Recycling shrinks an ordinary tier once its cached count exceeds floor(4 * width / 3), transferring up to one width of entries to its master; a full ordinary master destroys overflow. These are idle cache bounds, not limits on active allocations.

Payload capacities, excluding reserved left padding, are:

TierCapacityUse
Small4 KiBSmall ordinary payloads and control messages
Medium32 KiB in S1/S2; 64 KiB in M1/M2/L1/L2Framing, retained queues including paused Mux children, and best-fit UDP reads
Large64 KiB in S1/S2; 128 KiB in M1/M2/L1/L2Ordinary event-loop read buffers and larger working payloads
Splice storage32 bytes of control storageExplicit splice wrappers; logical body size is independent of allocation size
Splice payload limit1 MiB in every profileSocket read limit and required new-pipe capacity

S1 and S2 share buffer sizes but retain different profile-based local and master cache capacities; S1 caches fewer allocations. Higher profiles retain 64 KiB medium and 128 KiB large buffers. Their independent splice target remains 1 MiB. NIO ordinary TCP reads use the large tier directly. Linux NIO UDP reads select an ordinary tier through best-fit allocation when a positive size hint is available. Medium buffers also serve nodes and retained queues. Mux selects the smallest fitting ordinary tier; a full 1 MiB frame requires dedicated ordinary storage in every profile. The wire payload maximum remains 1 MiB.

bufferpoolGetBestFit() selects the smallest ordinary tier that satisfies both payload and padding requirements, comparing actual sizes across small, medium, and large. If no tier fits, it creates a dedicated padded buffer that is destroyed on return. bufferpoolTryGetBestFit() accepts a 64-bit computed length and returns NULL for unrepresentable geometry before using the same allocation path. Neither helper shrinks an existing buffer. Splice allocation stays explicit.

A successful best-fit allocation already satisfies the requested payload capacity and left padding; callers need no undersized-success recovery branch. Use bufferpoolGetBestFit() when caller-established bounds guarantee representable geometry, including padding and alignment; otherwise preserve bufferpoolTryGetBestFit()'s geometry refusal. These guarantees describe the returned buffer at allocation time. Incoming or subsequently shifted buffers still need the applicable capacity and prepend checks.

sbufCreateWithPadding() makes a separate ordinary sbuf_t and follows the same fatal heap-allocation policy as pool refill. Validate computed geometry with sbufTryComputeCapacity() before creating storage when an unrepresentable request must be refused locally. Destroy this direct buffer with sbufDestroy() after its last user has stopped.

The 128 KiB tier is not a maximum payload or total-memory limit. Smaller pooled allocations do not cap process memory, kernel pipe memory, frame sizes, or dedicated ordinary allocations. Frequent large materializations can incur allocation/free work; these sizes make no throughput guarantee.

Materialization callers must request the complete logical payload plus required headroom, including real prefixes, residual padding and any destination bytes already present. Low-level conversion helpers do not grow caller storage. For example, 600 KiB requires dedicated ordinary storage with a 128 KiB large tier; a pipe with 1 MiB capacity holding only 20 KiB needs storage for its logical bytes, not its kernel capacity. Dedicated buffers above all tiers are destroyed on recycling, while consumed splice wrappers return to their separate splice cache.

Each tier refills at most four buffers at a time, further bounded by half the local cache capacity and its available slots. Pools whose previous refill batch was smaller than four retain that smaller batch. Cache capacities and shrink thresholds are independent of this refill limit.

Fresh sbuf_t allocations with more than 2 KiB of physical buffer storage, including reserved left padding and alignment rounding but excluding the header, are prefaulted in shiftbuffer.c. Release builds write volatile zero bytes at 4096-byte strides and at the final byte, covering trailing unaligned pages. Debug builds retain their existing full-storage diagnostic fill, which already touches every page. The supported minimum page size is assumed to be 4096 bytes; no OS page-size query is performed.

This applies to newly allocated pool buffers, dedicated buffers and replacement allocations during growth. Pool checkout and recycling do not repeat the writes. Splice wrappers use only their physical padding and control-storage size, never their logical pipe payload size. Length, padding and ownership rules are unchanged; callers must still initialize payload bytes before use. Prefaulting pays possible first-write page faults during allocation, may populate unused capacity, and does not pin memory or guarantee a throughput improvement.

bufferpoolGetSpliceBufferStorageSize() and bufferpoolGetSpliceBufferPadding() expose the splice geometry. bufferpoolUpdateAllocationPaddings() accepts separate large, medium, small, and splice padding arguments. Global chain-padding updates cover all four tiers, and device pools inherit each tier's padding value from their worker pool. Finalize padding before creating the splice inventory or populating local caches; splice padding cannot change after inventory initialization.

The existing lineGetBufferPool(), getWorkerBufferPool(), and getCurrentWorkerBufferPool() accessors expose every tier through the same pool, including bufferpoolGetMediumBuffer() and bufferpoolGetSpliceBuffer(). lineReuseBuffer() and reuseBuffer() use the same recycling path as bufferpoolReuseBuffer(), subject to their existing worker ownership requirements. Ordinary recycling clears flags and length. Splice recycling preserves its representation/ownership flags and pipe, clears payload, and restores control-storage capacity and the original padding cursor.

bufferpoolGetSpliceBuffer(pool) returns a complete buffer from the local cache, shared master or bounded lazy growth. A refusal returns NULL with ENOBUFS; unsupported builds use ENOSYS. Handle this before accessing the buffer, normally by choosing ordinary storage. Bypass uses the same bounded global inventory without local caching. Only checkout/refill may grow the inventory; returning a healthy buffer never creates, resizes or closes its pipe.

If you need a buffer bigger than the current one for an append, use sbufReserveSpace() (it reallocates and copies only when necessary) rather than hand-rolling growth.

sbufCreateSplice(pad_left) directly allocates an empty buffer with exactly 32 bytes of payload capacity, plus left padding rounded up to a 32-byte boundary. Its payload capacity stays at 32 bytes regardless of the CPU cache line size. The buffer starts with zero length and only kSbufFlagSplice set; sbufDestroy() physically frees this unmanaged allocation. Inventory entries are acquired through the pool APIs and preserve their flags while cached.

Ordinary TCP and UDP receive sizing​

NIO ordinary TCP reads take the pool's large buffer directly and call recv() with its full writable payload capacity, without first querying FIONREAD. This also applies to an ordinary receive after a failed pipe allocation or a splice attempt that makes no progress. A receive may return fewer bytes than the capacity; remaining stream bytes stay queued for later reads. Its actual return value determines the published length, and zero confirms TCP EOF. Buffer capacity does not define TCP application message boundaries.

On Linux, UDP uses FIONREAD as an allocation hint for the full size of the next datagram, not the sum of queued packets. bufferpoolGetBestFit() requests that complete size. It can select a larger tier or dedicated storage if the datagram exceeds a custom pool's large tier. Each receive still delivers one datagram. The socket has one serialized reader between the query and receive.

UDP best-fit reads request at least the large tier's reserved left padding, so a smaller tier with insufficient padding is ineligible. A failed or zero size query falls back to a large-buffer receive. Zero never skips the receive: it may represent an empty UDP datagram or transient readiness. The actual receive result determines the published length, and Linux UDP truncation detection still drops incomplete datagrams.

TunDevice uses its separate raw-IP/GSO reader and buffer sizing. Other NIO socket types and non-Linux/IOCP receive paths retain their own allocation policy.

Sizing Destination Buffers for Untrusted or Variable Payloads​

When sizing a destination buffer from an untrusted or variable payload length:

  • Use bufferpoolGetBestFit() (or another checked growth operation) rather than assuming a fixed buffer tier fits the input. Supply the left padding required by the onward buffer contract as well as the payload capacity: when no pool tier fits, best-fit creates a dedicated buffer with exactly the requested padding. Request zero only for a locally consumed buffer or a proven zero-padding destination.
  • Compare the requested length with actual writable capacity (sbufGetMaximumWriteableSize()) in ordinary Release control flow before any assert-only length mutator (such as sbufSetLength()) or write.
  • Publish the sbuf_t length only after the producer has successfully written or validated that many bytes.

Buffer Ownership​

Buffer lifetime bugs are as common as line lifetime bugs. The rules:

  • A callback takes ownership of the buffer you pass it. After tunnelNextUpStreamPayload(t, line, buf) (or the downstream form), buf is no longer yours. Do not read it, reuse it, or free it.
  • If you keep a buffer, you own it. You are responsible for eventually recycling it with bufferpoolReuseBuffer / lineReuseBuffer, or handing it to a callback.
  • On an error path that still owns a buffer, return it to the pool (or the line) before you close. Do not leak it, and do not touch it after the line dies.
  • If a re-entrant callback killed the line (a false from lineCallWithRef() / lineCallWithRefWithBuf()), only recycle buffers you still hold; do not touch the dead line's state to do it.

ContextQueue owns enqueued contexts until pop or destruction. contextqueuePop() returns NULL when empty; otherwise ownership transfers to the caller. contextqueuePush() terminates on deque insertion failure.

After transferring or recycling a context's payload, contextDropPayload() clears its payload pointer in every build configuration. It does not free the buffer. contextReusePayload() and bufferStreamPushContextPayload() perform this bookkeeping so later context destruction cannot recycle the payload again.

BufferStream representation, inspection, and extraction contracts​

buffer_stream_t accepts ordinary resident buffers only. Use its views, CRLF searches, exact/at-least/full reads, and IdealRead() only on ordinary input. Logical stream entries are storage boundaries, not protocol records. Push owns its input and preserves bounded ordinary coalescing: a positive input of at most 4 KiB may append to a tail with sufficient spare space. Exact reads preserve checked lengths and the explicit use_left_padding budget. Deque insertion failure terminates during push or exact-read remainder retention. Both stream implementations operate exclusively on bytes and owned buffers. Fragment assembly belongs to the device reader session; no claim, callback, sidecar token, or hidden padding metadata follows a packet through buffer transforms.

bufferstreamMoveExactBytesTo() validates the source range and destination append space before consumption. It appends into sufficient caller-owned ordinary storage without growing/replacing it or spending additional headroom.

Views and searches consume no bytes. The delimiter searches inspect all buffered ordinary bytes, including delimiters split across entries. bufferstreamFindCRLF() returns the CR offset; bufferstreamFindDoubleCRLF() returns the offset after the delimiter. No match leaves the output offset unchanged. HTTP parsers remain ordinary-only. HttpClient and HttpServer do not advertise splice support. HttpProxyServer establishes an ordinary-memory boundary at payload admission: only a complete, immediately forwardable splice wrapper in ready CONNECT/fallback relay bypasses materialization. Every input, output, deferred and active incoming session slot is ordinary. Parsing, transitions, Pause and reentry materialize the complete delivery through the shared splice read helper, preserving onward padding. The temporary ordinary input is at most P + D logical bytes and meets the existing remainder allocation bound based on max(small_capacity, 2 * D), plus padding, alignment and sbuf header. Existing admission limits what survives return; conversion adds no persistent allowance and makes no RSS or copy-cost guarantee.

Fixed-header splice stream​

splice_stream_t in ww/bufio/splice_stream.{h,c} owns a FIFO, an explicit active head, an immutable header size and a trailing resident header cache. It borrows its owner-worker pool. Construction checks object allocation arithmetic. Mux parents use an eight-byte header cache; Mux children have no receive stream. Bgp4Client and Bgp4Server use a 19-byte cache for marker, length and type. BGP OPEN inspection consumes only ten fixed bytes and up to 255 optional bytes through representation-aware range helpers. Application bodies remain eligible for private-pipe extraction and per-frame delivery. BGP uses a fixed 2,162,705-byte logical budget per direction and a 1,024-frame output cap; incoming fragment count is unrestricted. Both are ordinary middle nodes on a borrowed normal line: default Pause/Resume callbacks forward every notification, including repeats, and complete admitted frames drain without waiting for Resume. Per-direction pump guards preserve FIFO across reentrant callbacks. The receive stream retains incomplete input. Each encoded batch is prepared before its first handoff, and the output FIFO retains queued frames during reentry. Every input still undergoes its applicable retention checks before callbacks, including reentrant input. Ongoing framing prevents permanent bypass.

splicestreamDiscardFrame() consumes the cached header and exactly the requested body bytes without producing a payload buffer. Its complete-header/body preconditions match frame extraction. Resident bytes are skipped; Linux private-pipe bodies drain to a lazily opened, close-on-exec /dev/null descriptor owned by the stream. Short transfers and EINTR preserve progress. Unavailable sink/unsupported splice falls back to bounded 4 KiB scratch reads; missing claimed pipe contents remain fatal. The next header refills only after the selected body is consumed. Destruction closes the sink and releases every retained input through the owner-worker pool.

SpeedTestClient and SpeedTestServer use a 48-byte splice header cache and advertise splice support. Client verify-payload defaults to false and HELLO flag bit 8 selects the same policy at both endpoints. Unverified DATA consumes its body through frame discard; verified DATA and control bodies materialize only after enough bytes arrive. Partial frames return to the event loop. Each line bounds incomplete-frame charge to 2 * (16 MiB + 48), attempting beneficial compaction before refusal. Headers, lengths, stream IDs, sequence statistics and completion reports are always processed. Unverified senders initialize ordinary bodies to zero; verified senders generate the deterministic pattern. JSON summaries expose verification_enabled, and unverified payloads do not increment valid_packets. UDP keeps its per-datagram path.

Their private frame encoders receive DATA sizes already bounded to 16 MiB by client settings or the accepted HELLO; control bodies are fixed and at most 72 bytes. The 48-byte prefix and validated pool padding therefore have representable allocation geometry. Assert these caller bounds instead of adding nullable frame allocation or duplicate UDP-size checks. The shared header readers still validate external input, and report/HELLO dispatch establishes their exact body sizes before private readers run. Sender pacing preserves wide byte counts when converting them to elapsed microseconds and caps unrepresentable delays before narrowing.

splicestreamPush() takes ownership on every result. It checks accounting and reserves FIFO metadata before consuming any new header bytes; representability refusal recycles the submitted input while preserving existing bytes and costs. Heap exhaustion is fatal. Empty inputs recycle immediately. Ordinary bytes, real resident splice prefixes and private pipe bodies maintain FIFO order. Push physically removes only missing header bytes from sources and recycles exhausted sources. The header cache owns only bytes.

splicestreamPeekHeader() returns a borrowed cache pointer only when complete; the pointer expires on mutation. Repeated peeks perform no I/O or allocation. splicestreamLength() includes cached header bytes; splicestreamBodyBytes() returns zero until the header is complete, then counts all following bytes. Neither peeking nor waiting for a body refills a second header.

splicestreamMoveFrame(stream, destination, N) consumes the current cached header and exactly N body bytes. Header readiness and complete body availability are caller preconditions, together with an empty destination; Debug asserts these without repeating the decoder's availability checks in Release. The supplied destination transfers ownership; always use the returned pointer because fallback may recycle/replace the supplied allocation. A NULL destination selects ordinary output, including after failed pipe checkout. The result preserves full onward pool padding. Resident-only ranges remain ordinary, including real prefixes of splice wrappers. Representation selection examines only the requested body bytes, in the active head and queued buffers; later pipe bytes do not make an earlier resident-only body use a pipe. splicestreamMoveFrameToOrdinary() has the same caller preconditions and retains fatal checks for ordinary destination type and sufficient storage before consuming; it never grows or replaces the destination. After the body completes, the stream clears its cache and gathers only the next header. N=0 consumes one zero-body frame. With header size zero, extraction is an exact body move with no header discard or look-ahead.

splicestreamMoveFrameChunk(stream, destination, N, finish_frame) moves exactly N available body bytes using the same ownership, representation and padding rules. The caller tracks the declared remaining body length. With finish_frame false, the complete header stays cached and no next header is gathered; only the final body chunk may set it true. splicestreamDiscardFrameChunk() follows the same header-completion rule without producing a buffer. A zero-byte final discard consumes an empty frame without allocating a payload. Stream length continues to include the cached header until completion; body bytes already consumed do not remain in its logical length. Source capacity and charge follow the ordinary and splice consumption rules below. Mux uses these APIs for incremental delivery and discard; complete-frame users retain their existing extraction APIs.

Pipe-backed extraction can split one source pipe or combine several independently owned sources into a new private pipe. Whole suitable wrappers transfer directly. Ordinary ranges and real prefixes copy/write in order; pipe bodies use nonblocking pipe-to-pipe splice(). Positive short progress accumulates and EINTR retries. Only successful transfers advance lengths; body consumption never moves the cursor into metadata at buf + l_pad. No descriptors have two owning wrappers.

Pipe destination capacity is fallible: nominal F_GETPIPE_SZ does not prove slot availability. Creation/growth failure, unsupported destinations and destination pressure select complete ordinary fallback, not process termination. Allocate the final best-fit result first, drain only the actual assembled destination bytes, then append the still-unmoved source range. Return exactly N ordered bytes once, leaving unrequested suffixes intact. Missing claimed source bytes or invalid owned descriptors remain fatal invariants. Exact transfer primitives live in splice_buffer, independently of Mux and the event loop.

SoftIpLimiter identity and SpeedLimit shaping​

SoftIpLimiter materializes each complete delivery through sbufEnsureOrdinary() only while identity admission is undecided. Empty deliveries contribute no identity bytes and are recycled before buffering. Its BufferStream stays ordinary and replays the original identity bytes and coalesced tail intact. The committed phase is published before next Init; nested input retains its private pipe in the existing bounded FIFO behind initial replay. Established and passthrough payloads in both directions are opaque. Temporary inspection never sets permanent ordinary-read preferences.

Each admitted line records its IP row's generation. Pruning and recreating the same identity/IP pair creates a new generation; an old line must neither refresh nor release the replacement row. Concurrent lines admitted to the same live row share its generation and reference count. Generation allocation and table changes stay under the existing instance write lock.

SpeedLimit retains ordinary and splice entries in its existing directional FIFOs. Whole grants forward the original wrapper; partial grants pop the source, extract exactly the permitted range through sbufMoveRangeTo(), and reinsert its remainder before any callback. Splice splits use an independent destination pipe with complete ordinary fallback on pipe creation or transfer pressure. Ordinary ranges preserve onward pool padding. Each FIFO has explicit logical-byte, queue-capacity-charge and entry budgets, including empty entries. Failed admission recycles the rejected buffer and settles both queues before closing the borrowed normal line through its owner. Queue pop/extraction/reinsertion invokes no callbacks; remaining ownership and accounting are published before a slice is transferred. Reinserting a split remainder reuses the popped slot and no greater byte/charge cost, with no callback in between; use the ordinary push wrapper. Initial admission still retains its ordinary refusal path. Finish deletes both timers and destroys both queues before asserting their budgets empty.

UserController retained payload and delivery​

UserController's stream and datagram FIFOs retain original ordinary or splice buffers. Stream grants use logical sbufGetLength() and exact sbufMoveRangeTo() splits, preserving independent private pipes and ordinary fallback. Datagrams are never split; zero-length entries retain their FIFO position and transfer once without rate or quota cost. Reject datagrams above 65,535 bytes without charging, while preserving any smaller producer bound.

Both directions share line ceilings of 1 MiB logical bytes, 2 MiB actual allocation charge and 256 buffers. One user's queues in one AuthenticationClient share 4 MiB, 8 MiB, 1024 buffers and 1024 direction tickets; that client shares 64 MiB, 128 MiB, 16384 buffers and 16384 tickets. Each line has at most two direction tickets. These are resource bounds, not bandwidth settings. Queues and tickets allocate lazily. Owner-local budget primitives remain owner-local; synchronized user/client reservations account shared costs and roll back admission refusal without lock inversion.

Detached/prepared output remains part of retained costs until handoff or discard. Reserve split destinations and required continuations before the shared bandwidth/quota commit. A preview grants no ownership of credit; a concurrent worker or refresh may invalidate it. No fallible work intervenes between a successful commit and the neighboring Payload callback. Stream retention refusal discards the refused buffer and closes that line; datagram overflow drops only the new whole entry and preserves accepted FIFO contents. Required timer/continuation refusal closes either mode. Every still-owned buffer returns to its line pool, and transferred buffers are never recycled locally.

Reservation identity includes its runtime ticket. Removal and recreation of the same numeric user ID must not let late release subtract from the replacement account. Client aggregate cost stays reserved while old buffers remain retained, and the owner releases it once. See Part 7 for cancellation and shutdown ordering.

HeaderServer and KeepAlive framing​

HeaderServer materializes each complete delivery with sbufEnsureOrdinary() only while waiting for its port, TCP/UDP protocol, or PROXY protocol header. BufferStream remains ordinary, including coalesced application tails. It publishes the established phase before next Init; nested input then enters the existing opaque FIFO behind the original tail. Constant-port mode and established payloads in both directions preserve splice buffers. Temporary parsing does not set permanent ordinary-read preferences.

HeaderClient protocol modes snapshot exactly the selected source or destination protocol during Init, before next Init can rewrite the context. A one-byte header carries 6 for TCP or 17 for UDP; unset, ambiguous, or unsupported protocol flags close the borrowed line through its owner without next Init. The snapshot is zeroed before Finish or before bypassing and handing off the first prefixed payload. Port and PROXY modes keep no per-line state and use default Init/Finish. The active-node mask prevents another header. HeaderServer's protocol mode consumes the byte and changes only destination protocol before next Init; addresses, ports, and source metadata remain intact. This marker does not provide UDP datagram framing. In constant-port mode, HeaderServer publishes the destination context, destroys and zeros its line state, and bypasses itself before next Init. Parsed-header modes wait until next Init and the ordered initial replay finish. This keeps nested input behind the original tail before retirement. Both nodes relay repeated Pause/Resume once the destination side is initialized; neither retains consumer permission to gate output.

KeepAliveClient and KeepAliveServer frame every payload throughout the connection. The prefix contains a four-byte big-endian body length and one kind byte. Each frame carries at most 1 MiB of payload; length includes the kind, so body length is at most 1 MiB + 1. Oversized lengths reject immediately after the header. Their five-byte splice-stream cache consumes only frame length and kind; normal bodies can remain in private pipes. Encoders prepend only in advertised real headroom, preserving complete wrappers when one frame suffices. Larger payloads split through sbufMoveRangeTo(), with complete ordinary fallback on pipe pressure. No generic resident-buffer reserve/copy operation touches a splice body.

Each encoder publishes its active suffix before callbacks and serializes nested inputs behind it. Active suffix plus queued nested input share an 8 MiB logical reentry limit and at most 1,024 buffers, including the active suffix. Decoders serialize nested input under an 8 MiB logical reentry bound. Complete frames drain before checking the 16 MiB retained allocation charge; beneficial compaction precedes charge refusal. Validated frame lengths and draining guarantee that the incomplete remainder is smaller than 1 MiB + 5 bytes, including the cached header. Check this derived bound with a Debug assertion. Nested admission also checks the charge bound. Body lengths outside 1..1,048,577 or refusal close through the normal line owner instead of silently losing TCP bytes. Both nodes require L4 on both edges and borrow normal lines; a mixed chain does not send its worker packet line through them. Finish settles the stream, active suffix and reentry queue. Pause relays promptly while an admitted synchronous batch can complete. In both client modes, the first ping is due one ping-interval after downstream transport Est; waiting for it does not block application data or start a reply deadline. Repeated Est does not reset this schedule. Timer checks and outstanding monotonic reply deadlines continue during either directional Pause. Both nodes relay repeated Pause/Resume through the default callbacks. Due pings and pong replies do not wait for Resume. An unsent ping blocked by an active encoder or an outstanding pong leaves one due obligation without starting another reply deadline. Each parsed Ping sends its Pong directly; the decoder's existing reentry guard keeps nested input behind older frames under the same input bounds. No separate pending-Pong queue is needed. Finish stops the drain and discards retained input. The optional reply watchdog uses ordinary empty control frames and leaves body representation unchanged. Missing or late empty pongs close through the borrowed normal line's owner; the framing node does not recreate the line. KeepAliveServer remains the responder. Both nodes remain active throughout the line because every payload still needs framing and control processing; neither is eligible for permanent bypass.

Protocol storage and admitted batches​

APIServer's hard input-byte limit includes older complete/active request charges, raw backlog and the current partial frame. Its ordinary byte watermarks subtract only the current frame's copied header/body. Predictive pressure compares retained older request bytes plus the next complete frame against that same hard limit: use max_request_envelope + 4 before its header is known, then its declared complete size. Count already buffered bytes once. Request completion and output drain must release the hold and Resume without new input. An incomplete frame alone must remain able to receive its missing bytes. Preserve hysteresis, independent memory/fragment limits, absolute frame deadlines and bounded closure when late or coalesced input exceeds hard caps; advisory Pause is no reservation. Protect line lifetime across Pause/Resume, which may synchronously Finish the borrowed line.

AuthenticationServer is an exact-line response endpoint. Its queued and active backing responses share one stable budget: 1024 entries, 16 MiB plus the 20-byte envelope in logical bytes, and 32 MiB allocation charge. Reserved pop preserves active charge; a guarded drain publishes its cursor before callbacks and emits at most 16 KiB per stream chunk with full chain padding. Refusal closes that borrowed line toward its owner after settling all retained state. One bounded frame, growing message, serialized text and output chunk are construction scratch outside that budget; see the node reference for the precise envelope and overlap limits. Parser reentry storage is also bounded at each authentication endpoint. AuthenticationClient retains the ordinary 32 MiB charge, 1024-entry and maximum-wire-message limits, with 2 MiB nested headroom. AuthenticationServer grants those limits only with validated token evidence. Unvalidated server input is bounded to 16 KiB of frame payload plus the 68-byte envelope, 64 retained fragments, and allocation charge no greater than the larger of 128 KiB and one best-fit pool allocation for that envelope. Inspect the length/token prefix before waiting for a body, and revalidate at dispatch. Nested bytes and the older unread suffix share their applicable allowance; a valid earlier message does not authorize a large unvalidated successor. Compact oversized carriers and tiny-fragment runs before returning from admission. Extraction copies fragmented messages once. Server response capacity grows geometrically within the complete-response cap; old and new allocations can overlap briefly during replacement. The current synchronous delivery, one extracted message and temporary compaction overlap are outside the retained-input allowance. Empty input retains nothing. Publish the older suffix before callbacks so nested input cannot overtake it; Finish settles the remaining stream.

Choose storage by the work it represents: incomplete metadata/body, a protocol wait, an initialization/reentrancy ordering barrier, delayed replay, or an association's independent producers. An otherwise-ready synchronous TCP buffer must not acquire another output queue solely for Pause or Est. See Part 2's admission contract.

Ordinary middle-node timers and deferred drains may submit due output through advisory Pause while preserving FIFO and protocol readiness. Retained data still belongs to that node's hard budget: a receiving adapter's limit cannot protect storage that has not reached it. Pause requests and eventual Resume do not reserve capacity, and overload may close the affected connection. Node-specific gates described below are local implementation behavior, not a generic scheduling rule.

Middle tunnels that need an application-data backlog use a default budget of 2 MiB logical bytes and 1,024 buffers per retention budget. Count queued buffers and active backlog buffers still retained under that budget; multiple queues sharing it do not each receive a separate allowance. This is a hard admission limit, not a universal payload-size limit, a memory/RSS limit or a Pause threshold. Do not create a queue on an otherwise-ready synchronous forwarding path just to apply the default.

Protocol parsers, frame allowances, ciphertext shaping and transport/Mux storage retain their own explicit bounds. Explicit node-specific application-backlog limits take precedence over the default. For example, Socks5Server retains its 1 MiB / 1,024-buffer ordering limit per direction, including active retained tails and nested input together. Each negotiation message contains at most 513 metadata bytes; complete messages are consumed before waiting for more input. Fragmented metadata extraction copies only the protocol prefix, excluding application bytes after a complete CONNECT header. Empty TCP deliveries consume no staging entries; UDP-control tails are discarded after association acceptance. Socks5Client follows the default application-data limit for its upstream negotiation FIFO and, independently, downstream input retained for parser reentry or an older response backlog. The latter counts active stream bytes and queued sources together. Its 4,096-byte limit bounds input waiting for the first transport Est or incomplete reply metadata; prefix extraction does not merge an arbitrary coalesced body. Ready direct payloads are not subject to a backlog cap, and empty TCP input does not consume retention entries.

Socks5Client and Socks5Server materialize complete negotiation deliveries with sbufEnsureOrdinary() before parser or reentrant-input admission. BufferStream stays ordinary; complete coalesced application tails are replayed without charging them as incomplete metadata. Client upstream application storage can retain splice buffers opaquely. After TCP setup, direct payload and necessary FIFO forwarding preserve original buffers. Server input conversion continues through CONNECT-wait until its TCP-established phase; success-reply ordering is unchanged. Their ready direct TCP ordering queues drain through advisory Pause. The client still waits for SOCKS command success before releasing application data, and the server still places its one success reply before response bodies. UDP association producer control remains separate from these same-line TCP drains.

ConnectionFisherClient and ConnectionFisherServer materialize complete deliveries only before ordinary handshake/selection stream admission. Their five-byte markers are parsed as soon as complete; an incomplete marker retains at most four bytes. The following application body has its own inclusive 2 MiB allowance, independent of delivery fragmentation or coalescing. Client downstream selection ordering has that same byte bound; upstream waiting queues and server temporary input share 2 MiB and 1,024-buffer bounds with their retained input, including the server body held across next Init. Empty staged TCP input consumes no entries.

Their application FIFOs can retain exclusive private pipes. After protocol setup and the necessary FIFO drain, they release ordinary handshake streams and empty queue storage. Ready payloads use direct whole-buffer forwarding without the startup backlog cap or ordinary-read preferences. The client remains a junction between its borrowed main line and its owned selected child; it cannot remove that line mapping. Server next Init still waits for the first application payload. After Init and replay settle, the server destroys its full line state and permanently bypasses itself; ready ordinary and splice buffers retain their identity through dispatch. See ConnectionFisher retirement.

SOCKS UDP uses ordinary storage throughout: client application input is materialized before retention, and both nodes materialize every datagram before header decoding or framing. Client UDP application/control/relay and server UDP client/remote lines request linePreferOrdinaryReadBoth() on their exact lines. Client target preparation also requests it before local DNS retains input. A UDP-only server requests ordinary reads on its TCP control line during Init; a mixed service requests them when it parses an allowed UDP ASSOCIATE command. These preferences are permanent and advisory, do not propagate between associated lines, and never replace handling already-delivered or transformed splice input. TCP CONNECT does not request an ordinary-read preference.

TrojanClient, TrojanServer, VlessClient and VlessServer also use a 2 MiB logical-byte base budget for their necessary parser state. Input fragments have a byte bound plus bounded parser metadata; do not apply an output-datagram count limit to fragments of one incomplete frame. Empty queued datagrams consume output entries. A parser's active head, cached header and queued sources are charged exactly once.

Necessary retained stateByte limit
Protocol/order/replay or output backlog in these four nodes2,097,152 bytes; 1,024 output/backlog entries
Trojan UDP input, including partial-frame/header state2,162,950 bytes: 2 MiB plus the 65,798-byte maximum incoming frame
TrojanServer initial request assembly2,162,950 wire bytes
VlessClient TCP response assembly2,097,409 bytes: 2 MiB plus 257 response metadata bytes
VlessServer request/UDP input, including active head and cached metadata2,162,689 bytes: 2 MiB plus the 65,537-byte maximum UDP frame
VlessClient UDP input, including pending response/frame metadata2,162,689 bytes: 2 MiB plus the 65,537-byte maximum frame

Equality is accepted. Overflow or failed queue admission settles only the affected flow/association; private-pipe pressure instead uses the shared ordinary fallback. Remove consumed metadata before applying a remaining-body/backlog budget. TrojanServer needs at most 320 request-header bytes and VlessServer at most 278 before accepting or rejecting the request. Incomplete request metadata fits within each node's header cache; a complete header coalesced with body is parsed before the remaining-body budget applies. These logical limits do not measure RSS or descriptor use. Receiving adapters additionally bound entries or allocation charge for decoded datagrams, including empty ones. Mux bounds queue charge for retained stream chunks.

TlsClient's pre-handshake application queue and RealityClient's early application queue follow the default 2 MiB / 1,024-entry bound. They represent a real protocol wait, including application data admitted before TCP Est. Once protocol-ready, their plaintext drains progress without waiting for consumer Resume. Reentrant input stays behind the active plaintext remainder, and active reservations retain their byte/entry charge until consumed and released. Local backlog pressure has its own release path. Keep ciphertext-shaping budgets separate.

TlsServer also bounds pre-handshake backend plaintext to 2 MiB / 1,024 entries. Equality is admitted; overflow closes the affected line and releases its queued buffers. This limit applies even when the handshake deadline is disabled.

TlsClient and TlsServer relay advisory Pause/Resume without gating due record output. Configured record delays, FIFO order, local shaping watermarks and hard limits still apply. TlsClient's plaintext drain can wait for its own shaping backlog to clear; that local scheduling obligation is independent of consumer notifications. Its receive path also drains ready plaintext after flushing TLS output: inbound traffic can retire the final shaped records and cancel the timer that would otherwise wake that plaintext drain. RealityServer already relays notifications through the selected live branch while retaining its authentication, branch-cutoff and Finish guards.

TlsClient and TlsServer use owned-buffer ciphertext BIOs. Feeding ordinary input transfers its complete sbuf_t on every result, including admission refusal; empty input is recycled without an entry. Reads advance the owned buffers without copying into an intermediate memory BIO or moving the unread suffix. TLS library writes normally copy directly into padded pooled output buffers. Taking output transfers the allocation, which remains valid after later SSL calls or TLS-state release. BIO callbacks must never invoke another tunnel: drain owned output only after the SSL operation returns, with the usual line-reference and reentry checks. Successful per-line TLS initialization attaches both BIOs to its SSL object before returning. Later startup failures, including TlsClient's inner ClientHello generation, can then use normal line-state teardown. Only partial initialization before that attachment frees BIOs separately.

Each ciphertext BIO admits at most 8 MiB of unread bytes and 1,024 nonempty buffers. Equality is accepted; refusal consumes new input and closes the affected connection through its existing failure path. These are protocol-storage limits, separate from application backlogs and record-shaping storage, not RSS guarantees. Reset and destruction recycle all retained buffers through the BIO's original worker pool. An SSL context and its BIOs have serialized owner-worker access; the context owns their shared BIO method and must outlive them. OpenSSL and BoringSSL compile separate instances of the common BIO implementation and must never exchange library objects. Initial ClientHello delivery still assembles a complete flight for StreamFragmenter; takeover still exposes the exact unread wire tail. ServerHello routing inspects a non-consuming prefix across BIO entries. Neither uses a borrowed pointer into TLS-library storage.

Each main worker SSL context owns an immutable decoded copy of TlsClient's configured ECHConfigList and frees it with the context. ECH-inner scratch contexts have no copy. Ordinary fixed-SNI connections use it and require ECH acceptance; rejection must not release application data, retry the transport, or downgrade to GREASE. Raw arbitrary-hostname ClientHello generation remains fresh GREASE. Preflight measures the configured ClientHelloOuter flight. Handshake takeover bindings retain the emitted outer client random, even when BoringSSL switches to the accepted inner handshake. See configured ECH.

With verification enabled, TlsClient keeps its pinned Trust Anchor ID intermediate cache in each SSL context. The context owns the immutable certificate objects. A matching server response permits verification to supplement the peer's untrusted stack with those candidates; it never grants trust. Preserve the original root store, hostname and verification parameters when calling X509_verify_cert(), restore the borrowed stack afterward, and free only the temporary stack container. Never add these cached intermediates to the trusted store or enable partial-chain trust to support certificate elision. Keep the advertised IDs and cached certificates paired with the pinned source mappings.

TlsClient's per-node, per-worker SSL context owns at most two cached session references. Lookup and ticket callbacks run through explicitly admitted runtime SSL objects on that owner worker; preflight and generated ClientHellos must not consume or populate the cache. Derive the ticket-size limit from the worst-case fresh ClientHello preflight, reserving space for PSK framing and up to 31 bytes of inner padding with real ECH, and discard oversized tickets. Selecting a TLS 1.3 ticket consumes its cache reference before the connection can send it. Before offering a verified session, revalidate its certificates with the current roots, hostname and parameters, using cached TAI intermediates only as untrusted candidates. Discard invalid candidates; if none remain usable, perform a full handshake. SSL-context destruction releases cached sessions after its SSL users are gone; cache operations never retain a line or borrow a worker pool. See session resumption for identity scope, expiry and the static-RSA exclusion.

TlsClient installs an optional BoringSSL record-output reservation hook. Completed TLS 1.2/1.3 handshakes can seal nonempty application records directly into the owned BIO's padded output storage when no control flight or write retry is pending. The ordinary output path handles other cases, including a declined reservation. Reserve obtains writable capacity and any required queue slot before sealing; commit publishes only the actual ciphertext length and cannot allocate or fail. A sealing error cancels the reservation without retrying encryption. New output allocations retain onward headroom and prefer aligned ciphertext bodies; appends preserve existing tail coalescing. The byte and entry limits still apply.

A reservation is exclusive and synchronous within one SSL operation. Callbacks may query SSL state but must not start SSL I/O, replace its BIOs or output callbacks, or drain/mutate its reserved BIO before commit or cancellation. The library retains no output pointer afterward, and outward delivery still waits until the SSL operation returns. The hook uses the existing record sealer, so record padding is selected once and control-record ordering remains in BoringSSL. Its commits bypass BIO_write, including that function's byte counters; payload accounting follows the owned output buffers. TlsServer's OpenSSL path uses ordinary BIO writes.

When updating the vendored BoringSSL tree, port the record-padding patch before tunnels/TlsClient/RadkesvatPatches/record_write_buffer_callbacks.patch, then apply prefix_symbols.patch after all custom public API patches. Keep both record APIs mapped in the checked-in include/openssl/prefix_symbols.h; preserve upstream's internal and architecture-specific prefix headers as well. Normal builds consume those headers without a prefix generator. Rerun the upgrade checks in tunnels/TlsClient/my notes.txt. The output patch is incremental on top of the earlier local patches; successful patch application alone does not verify record ordering, retry behavior or ownership against the newer library.

An admitted input batch is not a newly allocated array of all decoded outputs. Decode and transfer each complete frame in order, continuing after Pause for the remainder of this accepted input while the exact lines remain live. Hold only an incomplete suffix or data genuinely blocked by protocol/reentrancy. Stop on Finish, malformed input or receiver closure; never exceed a receiver's hard limit just to finish the batch. Separately scheduled or deferred work follows the exact node's role in Part 2; ordinary middle-node output needs no consumer-pause gate.

Raw QUIC carrier and retention​

QuicClient/QuicServer have an explicit carrier-mode. datagram maps exactly one carrier Payload to one complete QUIC datagram without an outer envelope. stream uses a four-byte unsigned big-endian datagram length followed by its body. Its shared incremental parser has a fixed four-byte header cache and bounded partial body; zero or greater-than-1,472 lengths fail before body allocation. It handles arbitrary fragmentation/coalescing and serialized reentry. No adapter-type inspection or automatic carrier detection is permitted. The application side is a byte stream and preserves bytes, not Payload counts.

QUICHE receives borrowed input only during its call. Complete wire packets and decoded plaintext are staged in independently bounded owned events and delivered after the driver returns. WaterWall buffers use the exact line's pool and full chain padding. Framed output is constructed with its own four-byte prefix; both nodes advertise zero additional left padding and kNodeFlagBlocksSplice.

The default per-backlog limits are 2 MiB and 1,024 entries. Count queued and active application retention together with accepted but unacknowledged engine send data, including the opening byte. The engine uses at most 16 KiB per send chunk and a FIFO ledger of offsets, lengths and allocation charges; only fully acknowledged entries at the front release credit. Partial or out-of-order ACKs cannot free later entries across a gap. Each documented backlog also caps retained WaterWall allocation charge at twice its byte budget. Overflow closes the association rather than discarding arbitrary reliable bytes. These are storage-accounting limits, not a total QUICHE heap/RSS cap.

The fixed stream and connection receive windows are 16 KiB–16 MiB, no greater than the configured receive/storage budget, with receive autotuning disabled. The engine permits only application stream 0: the server advertises one incoming bidirectional stream, the client zero, and both zero application unidirectional streams. It bounds undecryptable packets at eight. QUICHE rejects more than 16 KiB of buffered CRYPTO receive data per encryption level after processing a frame. The enabled crypto-send check rejects growth past 16 KiB only when that level already has unsent data; it skips an empty unsent buffer and excludes sent but unacknowledged data. This is not a 16 KiB cap on total TLS/CRYPTO retention. The engine advertises active connection-ID limit two, disables migration, and retains strict pre-validation amplification factor three. The pinned library separately caps self-issued active plus retiring IDs at ten and may also retain the original destination connection ID; the advertised limit does not describe total CID storage. Fixed descriptor rings require (max-buffered-buffers + 1) * 24 <= max-buffered-bytes. Send-ledger backing charges 16 bytes per configured entry; each wire/plaintext backlog charges 24 bytes per entry, including empty-ring reservations. The configured maximum emitted datagram size is 1,200–1,452 bytes; ordinary QUIC packets may be smaller. Accepted framed input is independently bounded at 1,472 bytes. Keep these internal bounds aligned with the production engine when changing its pinned library or admission mapping.

HalfDuplex setup and direct forwarding​

Both HalfDuplex nodes remain exact-line endpoints after pairing. The client maps a borrowed main line to two owned transport lines; the server maps two borrowed transports to its owned main line. Permanent bypass would lose that mapping and Finish cleanup. Their aggregate producer holds and startup replay gates retain the endpoint flow-control responsibilities described in Part 2.

HalfDuplexClient must combine its 17-byte upload intro and complete first input into one ordinary buffer. It advertises 17 bytes of headroom and reuses ordinary input when those bytes fit. Insufficient headroom uses a best-fit allocation with full onward padding. Consume splice input through exact representation helpers, including any resident prefix before its private-pipe body; never treat logical length as pointer accessibility. Its separate download intro is ordinary. Both intros and the publication guard exist before callbacks. Empty input retains the same first-Payload timing. The existing Init/intro FIFO may own splice wrappers and retains its inclusive 2 MiB / 1,024 buffer policy. Later payload in both directions transfers the original wrapper. Application Pause/Resume maps directly to the initialized download child, including repeats. Before that child exists, the client remembers only a pending Pause; an intervening Resume cancels it. Child write pressure remains aggregated separately to control the application source.

HalfDuplexServer must materialize whole deliveries in unknown and waiting-upload states before parsing or retention. Its buffering slot is always ordinary, including body bytes accompanying an intro. Check combined lengths and reserve full onward padding before merging. Pair an available peer before applying the existing waiting-upload limit. Strip exactly 17 ordinary bytes and replay the retained body before direct traffic. After startup replay, upload and return traffic remain representation-blind; download-side extra input is recycled without reading its body. Conversion refusal removes any waiting map entry and releases retained input before finishing the borrowed half through its owner.

The owned server main line publishes startup ownership before next Init: a separate ordinary initial body, an Init/replay barrier, permission state, and a budgeted FIFO for later input. Both transport halves (including a PipeTunnel companion) remain borrowed. Reentrant upload must join that FIFO while the barrier is active, even when the role enum already indicates direct upload. The initial body retains its previous admission and is not charged to the new FIFO budget.

The later-input budget has inclusive 2 MiB logical bytes, 2 MiB canonical allocation charge, and 1,024 entries. Initialize it and attach its queue at their final stable addresses. Empty entries consume charge and entries. Materialize each retained splice delivery fully with checked best-fit storage and onward padding; refusal closes the association. Ready deliveries have no new cap or transport queue. The waiting-map policy and HalfDuplexClient's policy are unchanged.

Replay cannot run inside next Init or recursively inside initial-body dispatch. It waits while next is paused, detaches the initial body before handing it off, then rechecks exact main/upload/download life and associations. Keep the barrier active across that first handoff. Prepare the entire currently queued tail without outward callbacks, reusing a sole entry or coalescing multiple entries into checked ordinary storage. Release queue charges as ownership moves into this bounded scratch. Clear the barrier immediately before the final tail handoff, so any nested direct input follows all older bytes already handed to next. Do not loop through a FIFO that callbacks can continuously refill or require a future input to make progress. Materialization and coalescing overlap are not an aggregate physical-memory bound.

Next-side Pause publishes permission and source notification state before callbacks. Resume during Init/replay records permission without bypassing the barrier. Release source Resume only after older startup output is handed off and next remains writable. Publish notification state before callbacks and retain and recheck all exact association lines. Every close origin releases main-owned initial/FIFO storage, verifies empty budget usage, and detaches associations before outward Finish; internal refusal finishes all initialized affected sides, while a received Finish never reflects toward its sender. The owned main dies before its Finish handler returns. Preserve the existing sibling-close scheduling fallback.

PipeTunnel transfers the wrapper through worker messages without inspecting its body. The already-inspected intro is replayed as ordinary memory on the target worker. Private-pipe bytes survive source closure; canceled messages destroy the wrapper without borrowing a foreign worker's pool. No permanent ordinary-read preference is needed for either HalfDuplex node.

Reverse activation prefix​

ReverseServer sends one ordinary prefix per pair: a four-byte big-endian total length followed by random padding. Length includes the header, ranges from 4 to 65,536 bytes, and is selected afresh with the fast CSPRNG family. Server-only activation-min-size and activation-max-size default to 64 and 1,024 inclusive; strict integer validation enforces wire bounds and min <= max. ReverseClient uses only the received length and protocol bounds, never the configured range. No fixed magic bytes or recurring application framing are added. Random sizing is not a guarantee against fingerprinting; a plaintext length and timing remain observable without an enclosing encrypted transport.

The server builds the prefix in a separate padded buffer and hands it off exactly once even if the reverse consumer is paused. This startup exception applies only to activation, with no buffered user bytes attached. Publish a reentry barrier in both halves before inherited Pause notifications and keep it through the prefix handoff; nested Payload or Resume cannot release application data ahead of it. Finish cancels any unsent prefix. Receiver hard limits still apply, and the next transport may queue activation until it can physically write.

Retained application replay preserves FIFO through reentry and waits for output Resume. Each server half has one budgeted application FIFO with inclusive limits of 2 MiB logical bytes and 1,024 buffers, independent of RAM profile. Waiting data and startup retention share that budget; pop releases its cost before handoff. Nested admission during one prefix handoff or backlog-drain dispatch has the same finite byte and buffer bounds, preventing callbacks from endlessly refilling it. Empty TCP input is recycled without an entry. Refusal closes the waiting half or both paired halves through their owners. The separate activation buffer is at most 65,536 bytes. Reverse handshake fragments are compared incrementally and recycled; only a matched offset is retained. Cross-worker replay coalesces the admitted FIFO behind a synthetic handshake, without outward callbacks during preparation.

ReverseClient caches four header bytes and discards padding incrementally through sbufReadRangeToMemory() using at most 256 bytes of scratch. Never interpret pipe metadata as header/body memory, allocate the announced length, or forward padding to the destination. Validate length before consuming its body. Preserve an accompanying application's ordinary/private-pipe remainder and forward it after destination Init; later deliveries transfer intact. Nested application input while destination Init/initial handoff is active stays behind older data in one owned FIFO. The original initial body has an active reservation in the same budget as queued input: 2 MiB logical bytes and 1,024 buffers, with empty input consuming no entries. Release the reservation or pop cost before handoff. Preserve the first body's identity and queued private pipes. Nested admission across one initial dispatch separately has those finite byte and buffer bounds. Close releases the initial body, its reservation and all queued entries exactly once. These logical budgets are not allocation-charge or physical-memory guarantees; ready direct payloads do not enter them.

Client first-payload requests​

TrojanClient and VlessClient assemble their initial request and the entire first eligible application payload in one ordinary buffer. Reuse a locally owned ordinary body when the complete request and any first UDP frame header fit its advertised left padding. Otherwise allocate best-fit ordinary storage and append the source with the shared exact range helpers, preserving any existing resident prefix and fully materializing a pipe body. Do not spend another node's padding or leave the body in a splice wrapper for this first combined transfer. Later opaque TCP preserves buffer identity and splice representation where the existing helpers permit it.

Each client exposes first-payload-timeout-ms, a nonnegative integer defaulting to 400. A valid payload after next Init triggers the combined request immediately, even before transport Est. If no request has been sent when Est arrives, establish the deadline then; paused time counts. At expiry, direct TCP submits a header-only request even during advisory Pause. An owned UDP carrier waits for Resume while its output is paused. Zero disables deliberate waiting. An already-available eligible payload is still combined; expiry never forces it into a second send if the request has not yet been submitted.

Both clients relay repeated TCP Pause/Resume transparently; pressure state remains only for the owned UDP carrier's producer. A synchronous admitted input can complete its combined handoff while that carrier timer waits. Publish request admission before forwarding, cancel the timer, and never send a second header. Empty TCP input does not trigger a request or restart the deadline. UDP eligibility follows the protocol's own datagram rules.

Keep full onward padding in new allocations. TrojanClient advertises 331 bytes: the 68-byte UDP association request plus the largest 263-byte datagram header; its largest TCP request is 320 bytes. VlessClient advertises 280 bytes: its largest 278-byte request plus the two-byte UDP length prefix. UDP first output contains the initial request, one datagram frame header and that complete body. Later datagrams still prepend only their own frame header (at most 263 bytes for Trojan and two for VLESS). Header-only timeout output has ordinary best-fit storage with onward padding. No queue or extra timer is needed just to await Est or a peer response before outbound submission.

First-buffer construction can transiently add the actual request/frame headers to a selected retained payload's size. That finite construction overhead is not permission to hide a second retained output queue. Nested clients add headers and repeat assembly; a normal 1 MiB splice delivery plus 64 maximum-size Trojan headers adds only 20 KiB, within the 2 MiB retained-input base budget. One ordinary output buffer does not promise one syscall, TCP packet or TLS record.

TrojanClient variable headers and opaque TCP​

The initial request is 68 bytes for IPv4, 80 for IPv6, or 65 plus domain length, at most 320. UDP association uses 0.0.0.0:0, a 68-byte initial request. The first combined UDP output also includes ATYP ADDR PORT LEN CRLF for its datagram. Subsequent upstream datagrams are wrapped inline; insufficient headroom uses sbufMoveRangeTo() with full onward padding and preserves its returned pointer.

Trojan address inspection uses an allocation-free owning value with explicit incomplete/complete/invalid results; failed decode or capacity-checked encoding leaves outputs unchanged. Command, CRLF, port and datagram-size policy remain with the caller. Validating a reply source creates no owning routing context and never changes the application's destination.

UDP receive caches at most 263 header bytes: 11 for IPv4, 23 for IPv6, or 8 plus domain length. Consume only required metadata with sbufReadRangeToMemory(). Validate address, nonzero port and CRLF before extraction. The two-byte body length accepts the full 0..65,535-byte range; the frame adds at most 263 header bytes. Never retarget the application from a received source address. A zero-length body is a valid datagram, including as the first application payload. Oversized local UDP above 65,535 bytes is dropped without triggering the first request; malformed carrier frames close that association. Complete current-input datagrams continue after Pause; only an incomplete suffix or a genuine protocol/ordering barrier is retained.

VlessClient response transition and UDP framing​

The initial request is 26 bytes for IPv4, 38 for IPv6, or 23 plus domain length, at most 278. Target preparation establishes a valid address and nonzero port; Init saves a private copy that remains unchanged until cleanup. The request encoder can assert those preconditions. Keep its checked allocation for the combined request and first body: an arbitrary TCP delivery can still exceed representable buffer geometry after adding headers and padding. UDP frames contain a two-byte big-endian body length and no address. Bodies are 0 through 65,535 bytes. Local oversized datagrams are dropped without triggering the initial request. An empty datagram emits a 00 00 length prefix and triggers the initial request when it is the first application payload. A received zero length consumes its two-byte prefix and delivers exactly one empty UDP datagram; keep the association open and continue parsing the next frame in order. This differs from Xray's base VLESS implementation, whose writers drop empty datagrams and whose reader skips zero-length frames. Empty-datagram delivery requires a peer that supports it; nonempty framing is unchanged. Empty datagrams are valid at the UDP layer (RFC 8085 §5).

Response parsing caches at most 257 metadata bytes. Consume exactly the two fixed response bytes and the indicated addons before body delivery. Version must be zero; nonempty addons are accepted/skipped with the existing warning. TCP then stops interpreting headers; preserve a coalesced body in its representation without waiting for Est or building a separate ready-output queue. UDP gathers its exact length header and extracts one complete body at a time, including every complete frame in the accepted input after Pause. Retain the incomplete suffix.

After response completion, nested TCP body input uses the 2 MiB / 1,024-entry ordering bound, including any still-local active head once. Publish admission before callbacks and release accounting before body transfer. This entry limit does not apply to fragments of an incomplete UDP frame or to ready synchronous TCP forwarding.

Whole suitable bodies transfer directly. Split/mixed bodies use shared exact range movement and preserve the returned replacement pointer, unrequested suffix and full onward padding. Mutate only a popped, locally owned active head. Metadata, active input and queued sources remain charged once until consumed.

TrojanServer request inspection and retained wire bytes​

Initial parsing caches at most 320 resident header bytes. Preserve authentication and fallback policy: the first callback's logical length must include all 56 hash bytes, even if splice-backed; validate available hex/CRLF before authentication and cache successful lookup. Post-authentication malformed input closes the protected flow. Rejected unauthenticated input replays every inspected byte before its untouched tail, with replay ownership secured before fallback Init. Do not read opaque TCP body merely to fill a maximum-sized header cache.

After selecting and initializing CONNECT, ordinary body/reply forwarding is inline; retain only input needed to preserve ordering across branch Init or nested callbacks. UDP stages address/port/length/CRLF with at most 263 metadata bytes and accepts 0..65,535 body bytes in both directions. A complete current-input datagram is submitted through its selected backend even after Pause; the receiver owns bounded tolerance. Keep only incomplete input or actual initialization/protocol-ordering state. Replies preserve the same 65,535-byte body maximum and prepend at most 263 bytes.

The Trojan body bound is a framing capacity, independent of UDP socket/path or peer limits. Ordinary IPv4 UDP payloads reach at most 65,507 bytes without IP options. Preserve each accepted datagram intact: a frame may span carrier reads, but neither endpoint truncates the application body or splits it into separate UDP datagrams.

TrojanServer retains only cached address metadata while awaiting a UDP body. It constructs a routing context only for a new backend or a TCP target; reusing a backend needs no transient domain ownership. Each remote separately owns its immutable lookup key. New destination-domain storage follows the fatal heap-allocation policy. Address encoding checks its complete extent before writing and leaves splice bodies untouched.

A necessary association reply FIFO accounts encoded wire bytes and datagram entries under the 2 MiB / 1,024 limits. Live initialized backend replies have no Est waiter. Immutable replies already accepted into the client FIFO belong to that FIFO, not to a borrowed backend pointer. Its independent drain honors Pause; inline forwarding of an already-admitted backend reply follows the synchronous batch rule.

Delayed fallback retains a real bounded backlog, including inspected replay bytes. Its scheduled drain and narrow source-Finish final batch use exact representation-aware assembly, sole-buffer transfer where suitable, and complete ordinary fallback. Those independent/final drains preserve FIFO, completed branch Init and no-reflection obligations; advisory Pause does not block a due or final batch.

VlessServer request inspection and replay​

VlessServer borrows the client normal line and owns only its fixed-destination UDP backend. It caches at most 278 request metadata bytes, consuming only the next required field through shared range helpers. The first callback must contain 17 logical bytes (version plus UUID), regardless of resident/pipe boundaries; shorter or empty input takes fallback/reject. Cache successful authentication. Nonzero request addons remain unsupported once their advertised bytes arrive; post-authentication malformed input closes rather than replays. Remove accepted metadata before body budgeting.

One guarded mixed-input FIFO and popped active head preserve old body bytes across Init and nested input. TCP output and fallback replay share a 2 MiB / 1,024-entry upstream budget; the response-header/reentry FIFO has its own equal budget. Attach populated parser storage transactionally only when it becomes output. Parser bytes include cached metadata, active head and queued sources exactly once, without an output-entry limit on incomplete UDP fragments.

Fallback restores all inspected bytes before untouched tails before branch Init. No drain or final batch runs inside Init. Zero-delay admitted handoff follows synchronous input rules; due delayed or independently retained replay does not wait for Resume. Sole suitable buffers transfer directly; mixed batches use exact range movement with complete ordinary fallback. UDP gathers exactly two length bytes and extracts 0..65,535 body bytes with full onward padding. Zero delivers exactly one empty datagram and leaves the association open; continue with the next frame. Body extraction leaves following frames untouched. Complete accepted frames continue after Pause. UDP replies prepend a two-byte length prefix; insufficient headroom uses shared movement, preserving any resident prefix and private body. Empty replies emit 00 00; oversized local replies drop without closing the backend. Send the 00 00 response header exactly once, combined with the complete first nonempty TCP reply or first framed UDP reply in one ordinary buffer. An empty first UDP reply emits 00 00 00 00, containing the response header and datagram length. VlessServer advertises four padding bytes for the response header plus the UDP length prefix on the first datagram. Reuse ordinary input in place when its remaining headroom fits the response header. Otherwise allocate a best-fit ordinary buffer with full onward pool padding and copy through shared range helpers, materializing any private-pipe body. For UDP the combined buffer also contains the datagram length prefix. Later replies retain their normal representation. Est, Resume and empty TCP input must not generate a standalone response header; no timeout or Finish flush emits one. Publish the response-sent latch before the combined handoff to preserve FIFO under reentry. Est remains prompt and independent of reply availability and output Pause.

VmessCommon and VmessClient ordinary records​

tunnels/Internals/VmessCommon owns the shared VMess KDF, prepared UUID keys, AuthID/header operations, mode validation, response derivation and persistent body codec. Server credential vectors, candidate searches, authentication pumps and replay tables stay in VmessServer. Explicit timestamp/entropy inputs make client seals deterministic in independent vectors; production draws fresh request material at its first transmission commit. Mutable cipher/SHAKE contexts stay on the line's worker; temporary, session and node-owned key material is wiped at its lifetime boundary.

Request plaintext is at most 316 bytes and its full envelope at most 374 bytes. The client authenticates the fixed 18-byte response length before retaining a response plaintext of 4..259 bytes, at most 293 wire bytes including both tags. Validate the expected marker and exact outer command length, consume bounded options/commands without applying remote account changes, and preserve coalesced body bytes. The server continues to emit its ordinary 38-byte response. Response authenticated-length keys and nonces use request key/IV; response body encryption and SHAKE use derived response key/IV, matching the pinned peer's independent directional material.

VmessClient blocks splice for the expanded chain and advertises zero required left padding. Allocate ordinary output with full onward padding, combining the request with the first encoded body record. TCP records use at most 16 KiB plaintext; UDP uses one record per datagram, with empty local datagrams retained only in packetaddr mode. Reuse the codec's maximum-frame arithmetic: tags, authenticated-length offsets and worst-case padding all count. Drop native empty/oversized local UDP without splitting or request emission. An empty framed record is termination; verify its encrypted tag before accepting EOF.

Each client direction has inclusive 2 MiB logical, 1,024-entry and 8 MiB allocation charge limits. Active popped buffers and incomplete fragments retain reservations; compact fragments when that reduces retained storage. Release reservations and owned buffers exactly once on handoff, refusal or close. Separate TX/RX guards retain input only for incomplete parsing or FIFO ordering; complete decoded records are delivered incrementally without a pressure-only output queue. Server unauthenticated input stays limited to 64 KiB: a large early plaintext burst can exceed that wire allowance after framing during identification. No response acknowledgement barrier, implicit pacing or enlarged server limit hides that admission refusal.

Both endpoints explicitly pass their immutable encrypted-counter-policy to each body context. Default wrap permits data at all ordinals 0..65,535 and continues 65,535 to 0 under the same session key. This deliberately repeats the 16-bit AEAD nonce sequence for interoperability: it is not rekeying and does not preserve nonce uniqueness. strict reserves output ordinal 65,535 for EOF, delivers valid incoming data at that ordinal and then closes before another nonce use. NONE/zero are unaffected. The policy is local with no negotiation; a strict peer can close a wrap peer at its limit.

Advance once only after a successful record, keeping body and authenticated-length nonces on the same ordinal. Preserve pending length/padding state across partial input. SHAKE masking/padding continues across rollover without resetting session material or response parsing. Authentication failure and EOF remain terminal; never retry using another ordinal or policy. In wrap mode the counter is bounded modulo 65,536; strict retains the 65,536 exhaustion sentinel. No reconnect or automatic replay is introduced.

Packetaddr uses ordinary UDP command 2 with the exact outer domain sp.packet-addr.v2fly.arpa and port 0. This is the only zero-port request exception; never route that marker to DNS or an outbound connector. Every authenticated body record contains envelope family 0x01/IPv4 or 0x02/IPv6, its network-order address, a big-endian nonzero port, then exactly one datagram. The envelope occupies 7 or 19 bytes, which must be subtracted from the body-mode maximum application payload. It carries no domain encoding. Validate the complete envelope before routing and never split a datagram. A header-only envelope is an empty application datagram; an empty outer framed record is still EOF. Native UDP keeps its empty-drop/EOF rules.

Client packetaddr retains one immutable final IP/port and one owned carrier. Explicit local DNS may supply that IP; an unresolved domain is refused before transport Init. Unexpected reply sources are dropped/counted rather than delivered to another association. Server packetaddr owns one bounded destination backend per IP/family/port, and encodes replies from that immutable backend target. Queued encoded replies own all envelope/payload bytes independently of backend lifetime. Existing per-direction byte, entry and allocation budgets remain unchanged by the destination-count limit.

VmessServer ordinary records and bounded retention​

VmessServer blocks splice for the complete expanded chain. Its borrowed normal carrier owns incremental authenticated-header and record parsing; each native or packetaddr UDP backend is a separately owned normal line. Never copy VLESS's initial-callback UUID requirement or successful TCP bypass into VMess: its AuthID/header may be split at any byte, and encrypted records remain active for the connection lifetime.

The AEAD request prefix is 42 bytes, followed by at most 316 plaintext-header bytes plus a 16-byte tag. Validate the authenticated length before allocating or opening that header. Decode record lengths in wide arithmetic: authenticated-length framing can represent 65,535 plus its 16-byte offset. Keep parser length/padding state until exactly that body is consumed. Empty framed records terminate the protocol and must not become empty UDP datagrams; authenticate an encrypted terminal record before accepting it. Raw none (zero client configuration) is TCP only. A UDP reply must fit one record; oversized replies are dropped without splitting. Native UDP drops empty replies; packetaddr encodes their nonempty address envelope.

Each unauthenticated carrier retains at most 65,536 logical input bytes and 256 buffers under the node-wide auth-pending-max-bytes allowance. Consumed AuthID/header bytes remain charged to that allowance until authentication or fallback selection settles. Per-direction ordering and fallback queues have inclusive 2 MiB logical, 1,024-entry and 8 MiB allocation-charge bounds. Popped active buffers retain their reservations across callbacks. Incomplete bodies have one bounded ordinary allocation; completed bodies are handed onward before parsing later records. No queue is maintained solely because an ordinary TCP consumer sent advisory Pause.

The first response includes exactly one encrypted response header before its body. Both directions follow the configured wrap/strict policy above; unencrypted bodies have no AEAD counter limit. Terminal closure follows WaterWall's full-close model and does not promise delivery of unsubmitted application replies at strict exhaustion. send-termination=false suppresses only optional outgoing empty framed EOF; valid incoming EOF remains authenticated and accepted, and raw zero has no framed EOF.

Fallback restores every inspected byte before untouched carrier input and secures replay ownership before branch Init. Only unknown credentials or unavailable authentication before recognition can select fallback. Recognized time/replay, integrity, command/option and resource failures never do. Due fallback replay proceeds through advisory Pause; source-Finish final replay follows the same finished-side and reentry rules as VlessServer.

MUX parent streams and decoded child queues​

Mux uses this exact eight-byte wire header, separate from decoded host fields:

OffsetSizeMeaning
03 bytesUnsigned payload length, big-endian.
31 byteFrame type/flags.
44 bytesUnsigned connection ID, big-endian.

The inclusive payload maximum is 1 MiB (1,048,576 bytes) for every frame type, independent of RAM profile, buffer sizes and requested or granted pipe capacity. A maximum frame occupies 1,048,584 wire bytes; a preceding Open adds another eight bytes (1,048,592 total). Decoding rejects a larger declaration as soon as all eight header bytes arrive, even for unknown CIDs or types, and closes only that parent through normal parent-loss cleanup. After a valid Data header, the decoder delivers available body bytes incrementally; it does not wait for the whole frame. Data for an unknown or closed CID is discarded incrementally. Empty Data is consumed without a child Payload callback. Other frame bodies are also consumed incrementally, but control effects wait until their declared bodies end. Available body bytes and completed controls drain before retained incomplete input is checked against 1,048,584 bytes, including its cached header. Bytes not yet received do not count as retained input.

Open=0, Close=1, FlowPause=2, FlowResume=3 and Data=4 retain their meanings; child CID uses the full 32-bit range. Ping=5 and Pong=6 are parent controls independent of child lookup. Their payload is empty and CID bytes carry a 32-bit probe token, echoed unchanged by the responder. Each control occupies eight wire bytes; Data framing and splice representation remain unchanged. Disabled keepalive consumes controls without responding or acknowledging. The serializer uses mux_wire_header_t; decoded mux_frame_t has a 32-bit length and must never be copied as wire bytes. Each child is an ordered byte stream with arbitrary Payload boundaries. Both Mux decoders track the current Data CID and remaining body length, publishing progress before child callbacks. Extraction returns only available body bytes: do not shift away another eight bytes. One Data frame may produce several callbacks and paused children may retain several queue entries for it. Empty Data creates no callback or child queue entry. Data for a missing or closing child is discarded as bytes arrive. If delivery closes the child partway through a frame, discard the remaining body before decoding the next header. Look up the child again for each chunk; do not cache its state pointer in the frame state. Parent receive reentry appends behind current input and observes the already-published frame progress. After callbacks, physical references protect allocation life and logical life/state checks decide whether parsing may continue.

MuxClient owns normal parent lines and borrows children. MuxServer borrows parents and owns children created on Open. Splice support changes neither direction nor Finish/lifetime rules. Available Data chunks and complete controls drain before checking the logical incomplete-carrier overflow limit. If a parent fails halfway through Data, already delivered bytes stay delivered and accepted queued chunks retain their usual detached drain; the missing suffix is not reconstructed. Later frames still follow the current frame's body on the shared carrier, so incremental delivery does not eliminate transport head-of-line blocking. MuxServer treats each accepted nonempty Data chunk as child idle activity; no whole-frame wait delays the initial-to-active timeout transition or its refresh.

MuxServer reserves each admitted child in one atomic count across all parents and workers until its line state is destroyed, including detached and draining children. max-live-children defaults to 0, disabling the configured aggregate count ceiling. A positive value enforces that ceiling before allocation. max-children remains a positive per-parent limit. A nonzero aggregate ceiling must be at least both max-children and memory-fallback-max-live-children.

Fresh cached memory data still applies the configured high/low pressure watermarks and byte reserve when the aggregate count ceiling is disabled. Unavailable, unsupported, or stale memory data instead applies the positive memory-fallback-max-live-children ceiling, including when max-live-children is 0; a positive aggregate ceiling also remains in force. Nonfresh data cannot reopen a gate closed by fresh memory pressure. These gates affect new Opens; the separate queue-capacity budgets below continue to bound retained buffers.

Queue-capacity budgets​

Mux uses one queue-capacity charge for every owned buffer:

sizeof(sbuf_t) + sbufGetTotalCapacity(buf) + kSbufAllocationAlignment

Capacity includes reserved left padding. Ordinary entries therefore retain their allocation-based charge; splice entries charge logical sbuf capacity, without adding wrapper control storage or kernel pipe capacity. Inventory accounting is independent of this queue charge and does not force a valid retained source into ordinary storage.

The sbuf header and alignment overhead are each 32 bytes. For example, after a read delivers 100 body bytes into a splice wrapper with 64 bytes of left padding:

ValueBytes
sbuf->len, before any prepend100
sbuf->capacity, including padding164
Queue-capacity charge: 32 + 164 + 32228

The charge stays 228 bytes even if the private pipe supports 1 MiB. This formula does not add kernel capacity or the wrapper's control storage. Use sbufGetAllocationCharge() when the question is the wrapper's actual userspace allocation instead. An ordinary buffer holding 100 bytes still charges its full allocated capacity, which can be larger than its length.

The boundaries below have different scopes and units. A parent is one Mux carrier connection; its children are the streams multiplexed through it.

BoundaryDefaultScope and behavior at the boundary
Splice socket read request1 MiB in every profileUpper bound per read attempt. A positive short transfer produces a shorter valid buffer; remaining bytes wait for a later read.
Payload in one Mux frame1 MiBIndependent protocol maximum. A larger declaration closes that parent as soon as its complete eight-byte header is decoded.
Retained incomplete input1 MiB + 8 bytesReceived logical bytes, including the cached header. Available Data chunks and complete controls drain first; an oversized remainder closes the parent. Unreceived body bytes do not count.
child-buffer-limit128 MiB chargeOne child's queued decoded data. Reaching the limit closes that child.
parent-buffer-limit512 MiB chargeIncoming stream plus all attached child queues on one parent. At the limit, try beneficial incoming compaction, then shed largest queued children. Unrelieved pressure closes the parent.
parent-write-buffer-pause-threshold128 MiB chargeOne outgoing parent FIFO. Reaching it pauses child producers while the FIFO drains.
parent-write-buffer-resume-threshold4 MiB chargeRelease the outgoing producer gate at or below this charge while transport is writable.
parent-write-buffer-limit512 MiB chargeOne outgoing parent FIFO, including admitted frame batches. Equality is allowed; exceeding it refuses the new output and closes that parent.
detached-buffer-limit32–256 MiB charge, by RAM profilePer Mux instance/worker, for blocked child queues retained after parent loss. Reaching it closes the newly detached blocked child. L2 defaults to 256 MiB.
detached-child-limit4,096–12,000 children, by RAM profileCount of those detached blocked children on one Mux instance/worker. Reaching it closes the newly detached blocked child. L2 defaults to 12,000.

The named queue settings are JSON-overridable. parent-buffer-limit: 0 disables the combined receive limit; it leaves individual-child and protocol limits in force. The two detached limits also support 0 independently. Outgoing pause and hard limits stay positive; resume may be zero, with resume < pause < hard. The read size follows the pool/RAM profile, while the Mux frame maximum remains independent of it. See the MuxClient and MuxServer references for complete settings and profile tables. Connection-count, idle-timeout, and server admission settings remain separate from these buffer boundaries.

parent-buffer-limit (512 MiB by default) covers incoming frame assembly plus all attached child queues. The stream counter remains separate from the child aggregate, and their sum is checked at stable return boundaries. Available Data chunks and complete controls in a coalesced delivery drain first, even if its temporary charge exceeds the limit. A paused child retains chunks as they arrive, so one Data frame may create several charged queue entries. Cached header bytes count toward the independent 1,048,584-byte incomplete-input bound until the body ends, but the header cache and stream/queue metadata carry no sbuf charge. The declared body bytes still missing from the transport carry no retention charge. After an ordinary receive pass drains available input, a partial Data frame needs only its cached header and remaining-length state between arrivals. Any accepted chunks for paused children belong to their queues, not to frame assembly. Active splice-head body consumption reduces logical capacity and its charge; prefix consumption and ordinary cursor movement do not reduce capacity.

parent_receive_charge = incoming_stream_charge
+ sum(attached_child_queue_charges)

Each child reads its charge from its queue. Attached queues bind to a budget in the stable parent-only state; parent loss rebinds them to their worker's detached budget before any outward callback. Both budgets use SIZE_MAX in every dimension for accounting; the Mux policies below still decide admission and closure. Detached child count remains separate from queued entry count. Ordinary pop releases queue and aggregate usage before delivery, including during reentrant callbacks.

Reaching a finite receive limit first attempts to combine incoming fragments into one ordinary best-fit buffer, only if its predicted total charge is strictly lower. Compaction preserves the cached header and exact byte order. It does not run on every Push or rewrite child/output queues. Remaining pressure sheds the largest queued child, preferring the oldest on ties, and rechecks after every callback. Several victims may be needed. No victim or no progress closes only that parent through normal parent-loss cleanup. A zero parent limit disables this combined finite bound; it introduces no other aggregate cap.

child-buffer-limit remains 128 MiB by default and rejects equality. Outgoing parent queues remain separate: their default pause threshold is 128 MiB, resume threshold is 4 MiB, and hard limit is 512 MiB; hard-limit equality is permitted. Detached queues retain their per-worker settings and zero/unlimited meanings. The FlowResume threshold defaults to 512 KiB and counts logical payload bytes; FlowPause follows the local child pause state. Ordinary child candidates may still compact to a cheaper pooled tier; valid splice candidates are not individually materialized for queue pressure.

There is no worker-wide pipe-count or pipe-capacity quota. Other parents cannot force fallback through shared pipe reservations. These charges are not physical memory, FD, kernel page-reference, socket-queue or RSS bounds, nor a strict instantaneous memory ceiling. More retained splice data can use more descriptors and nominal kernel capacity under the same logical budget. Pools may cache empty pipes after recycling; lower charge does not prove those resources were closed.

sbufTryGetQueueCharge() checks the scalar formula, including 32-bit overflow; sbufGetQueueCharge() is the internal representable-result helper. splicestreamCharge() exposes source ownership, while splicestreamCompact() performs the explicit beneficial pressure operation. Queue entries are immutable; the mutable active head reconciles its charge after every consumption. Extraction settles source charge before a result is forwarded or charged to a child queue. Parser depth defers combined enforcement across nested delivery until available body bytes and completed controls have drained. Pressure cleanup prevents recursive enforcement and requires progress after Close.

Mux allocation and data flow​

For a Data frame addressed to a live child, the receive direction from the carrier socket follows this path:

Incoming-stream sbufs and all attached child queues share the parent's receive budget. A forwarded result leaves Mux ownership; socket-adapter write queues have their own ownership and are outside this Mux budget.

Object or storageWhen allocated or obtainedHow it is released
Worker buffer pools and cachesRuntime/worker setup; ordinary tiers and splice wrappers refill through their pools.Worker/runtime teardown after their users have drained.
Parent-only Mux stateParent initialization. Includes the child map, outgoing queue, and incoming stream.Normal parent-loss/Finish cleanup.
splice_stream_t and its eight-byte header cacheOne allocation per parent, with the cache trailing the stream object.Parent stream destruction; the cache is not a separate sbuf.
Child state and pending queueChild initialization. The queue requests initial room for eight entries and can grow.Child cleanup, or completion of its detached drain.
Ordinary sbuf or splice wrapperChecked out from the owning worker's pool when data or framing needs it.Forwarded to its next owner, recycled when consumed/discarded, or physically destroyed by pool policy.
Private pipe pairCreated on demand within the global ceilings and exclusively leased when its wrapper is checked out.Consumption/discard returns the lease to the shared inventory. Healthy pipes close only at final inventory teardown.

Opening a child does not allocate a pipe for it. Queue-entry storage grows as needed; the initial eight-entry reservation is not a maximum number of buffered chunks. Each splice wrapper still owns its own pipe exclusively.

  1. Read into an owned buffer. The event loop checks out a wrapper containing the normal 32-byte sbuf header, left padding, and 32 bytes of control storage. It moves socket bytes into the private pipe before handing the buffer to a callback. Only actual positive progress becomes its logical length/capacity. Ordinary fallback instead obtains resident storage from the buffer pool.

  2. Gather the Mux header. The incoming stream takes ownership and physically consumes only the missing header bytes into its eight-byte cache. Three bytes arriving first, then five bytes plus part of the body, complete the header without requiring the body to become resident. An exhausted source is recycled; any remaining body stays in its buffer or pipe while waiting.

  3. Extract available Data bytes. A complete header is required, but the whole body need not have arrived. Consume at most the current frame's remaining length and publish the new remainder before callbacks. A suitable whole source buffer can transfer directly. A chunk spanning inputs or using part of an input gets an independent result assembled in order. Pipe bodies can move pipe-to-pipe; resident prefixes and ordinary sources copy or write as needed. Use the returned buffer because ordinary fallback can replace the proposed destination. Keep the current header cached until its last body chunk is consumed.

  4. Deliver or retain for the child. Writable children receive the result immediately. Paused children retain the chunks in FIFO order; a wire frame can occupy several queue entries. Source charges settle before the result is forwarded or added to a child queue. A split source remainder and a retained output each contribute their own charge; a whole-buffer handoff is not charged twice.

  5. Enforce receive retention after available input drains. Mux adds incoming stream charge to attached child queue charge. Under pressure, only a strictly cheaper ordinary representation justifies combining incoming fragments. After any compaction, remaining pressure follows the largest-child/oldest-tie and parent-close policy in the limits table. Compaction preserves the cached header. It does not run on every Push or rewrite existing child/output queues. Ordinary child candidates retain their separate smaller-pool compaction behavior; a large kernel pipe alone does not cause a splice child buffer to be materialized.

  6. Encode the opposite direction toward the parent. A fitting child payload receives its frame header in reserved left padding and can go directly to a writable parent when ordering permits. Paused admitted output, including Pong replies, shares one FIFO and charge limit inside Mux. New probes remain due until direct handoff is possible. Pops release charge before forwarding. A blocked writer is paused after admission; the outgoing pause threshold additionally pauses all children. Local holds resume at or below the resume charge threshold while the parent is writable, respecting other pause reasons. Large splice inputs are staged as complete ordered batches; admission precedes callbacks and Open precedes the first Data frame. A failed batch cannot publish just its first frames.

  7. Consume, recycle, or finish. Once forwarded to an adapter, a buffer leaves Mux's queue accounting. Socket writes may send only part of its real prefix or pipe body; the write queue owns the remainder. Recycling discards unread body bytes and may cache a healthy empty pair. A wrapper has no source-socket reference, so closing its original reader cannot invalidate its private body. Parent loss discards its incoming stream and outgoing FIFO; eligible already decoded child queues can finish under the detached-child budgets.

Mux buffer edge cases​

SituationResult
Fixed inventory is exhausted at socket readCheckout recycles the unused wrapper and ordinary socket reading takes over for that delivery. Splice mode remains available for later reads.
Lazy pipe creation or sizing failsReject the candidate, retain earlier full-sized acquisitions and stop that refill. A shared one-second cooldown bounds retries; ordinary fallback handles exhaustion.
Socket-to-pipe splice returns EAGAIN or zeroRecycle the empty wrapper and try an ordinary nonblocking receive. That receive distinguishes no-data-yet from EOF and handles TCP urgent-data boundaries.
Destination pipe fills during frame assemblyReturn a complete ordinary fallback, preserving bytes already moved and appending the remaining source bytes exactly once.
A read of claimed private-pipe bytes is short or interruptedAccumulate positive progress and retry EINTR. EOF, EAGAIN, or another failure before the claimed range is complete remains a fatal invariant. This is different from a short socket read.
One small splice buffer would become a larger pooled ordinary bufferSkip pressure compaction when it would not strictly reduce total incoming charge. Kernel pipe capacity alone does not justify conversion.
One delivery contains complete frames and a partial Data bodyDrain available Data bytes and complete controls before checking combined retained receive charge. The limit is not an instantaneous ceiling on the current delivery.
A configured parent budget cannot fit retained incomplete input and child queuesAfter beneficial compaction and available child shedding, close only that parent; do not loop indefinitely or abort the process for ordinary limit pressure.
A child dies while its encoded data is queued to the parentThe parent still owns the admitted output and preserves its ordering until sent or discarded by parent cleanup.
Parent loss interrupts Data after some chunks were acceptedDelivered prefixes remain delivered. Eligible queued chunks detach and can drain independently, subject to their per-worker charge/count limits. The parent's remaining input and outgoing data are discarded.
A callback closes a child or parent during delivery or pressure recoveryHold the required temporary line reference and recheck logical life/state before continuing. A reference protects allocation life, not permission to use destroyed state.
Empty parent input or a zero-length Data frameEmpty incoming sbufs recycle. A zero-length Data frame is consumed without a child Payload callback or child queue entry; it is neither EOF nor Close.

The queue-capacity formula and ownership transitions do not promise that physical RAM, descriptors, or fixed inventory capacity fall when a charge is released. Runtime I/O failures and the fatal claimed-byte invariants remain separate from configured queue overflow and its connection-level cleanup.

MUX outgoing parent queues​

Inputs from zero through 1 MiB inclusive keep the in-place single-buffer path; zero emits one Data header. The frame cap limits payload, not a local callback. Large ordinary inputs keep the ordinary aggregate encoder. A large splice input, including a real resident prefix, splits into independently owned Data bodies of at most 1,048,576 bytes. A real 1 MiB pipe body plus a resident prefix can exceed this cap and still requires splitting. The encoder stages the complete batch before any callback, with Open before first Data for a new client CID. It uses the shared exact range-transfer fallback; no temporary stream is allocated around one encoder input.

Batch preflight includes checked wire/frame counts, existing parent backlog, staged queue-capacity charges. Pipe estimates use completed logical sbuf capacity, not the empty wrapper or nominal kernel capacity. Reserve the remaining frames' ordinary allocation costs before selecting expensive pipes, so greedy pipe choices cannot refuse a batch that fits as ordinary. Reserve all parent FIFO entries before publication. Failure recycles the whole local batch and remainder, publishes no Open state or output charge, and closes only the affected parent.

After admission, all frames belong to the parent. Publish wire state before gate notifications or payload callbacks. The guarded pump preserves FIFO, including Ping/Pong, and stops output immediately on Pause. Resume continues the admitted remainder; nested payload or reentrant child Close queues after it. Parent death discards undelivered frames. Quiescence admits no new stream work, child, batch or protocol output.

parent-write-buffer-pause-threshold defaults to 128 MiB, parent-write-buffer-resume-threshold to 4 MiB, and parent-write-buffer-limit to 512 MiB of queue-capacity charge. Pause and hard accept strict integers in [1, INT_MAX]; resume accepts [0, INT_MAX]. An omitted resume defaults to 4194304 (4 MiB) independently of the effective pause threshold; explicit values are honored. Require 0 <= resume < pause < hard without clamping; diagnostics name all three effective values. Zero requires an empty outgoing queue and writable transport; a positive resume threshold permits release before the queue empties. Every split splice batch must fit the hard limit even when the transport is writable; unusually small limits can therefore reject a large splice delivery before any frame is sent. The existing fitting single-buffer direct path remains eligible for immediate delivery without FIFO admission. Gate release occurs at or below resume while writable, before selecting the next buffer and only after the previous callback has returned. Keep the pump guarded during fanout so reentrant output follows the FIFO. Renewed pressure interrupts Resume; the intrusive FIFO of held children releases only that subset, even if the aggregate gate never activated. A writer held again joins the tail. Snapshot references protect every member before callbacks, and current line life, attachment and mux state are checked before use. Startup completion reconciles reasons separately from emitted permission. Detach and terminal transitions remove membership before destruction; cleanup does not send Resume. Peer-flow and terminal-close reasons stay independent. Quiescence cannot release producers.

Individual holds are applied after complete ordinary/splice admission while the transport is paused, or when delivery leaves it paused. Control submissions alone do not acquire writer holds. Neither this policy nor a new child changes the hard-limit accounting or FIFO ownership. Check low water after delivery returns, not between subtracting a popped buffer's charge and delivering it. A release that raises charge above low water stops; writable draining supplies the next opportunity. An empty writable FIFO also releases ready held children.

These are per-parent retained charges, not physical memory or transport capacity. The thresholds do not promise continuous sender output or increased carrier bandwidth. Already-admitted Data is not withdrawn by peer FlowPause; the default 128 MiB child and 512 MiB aggregate receive budgets still apply. A zero-child ordinarily exhausted client parent waits for its final FIFO frames to leave before orderly close; crossing resume is not a terminal drain barrier. Watchdog-expired idle parents instead discard blocked final output and close without waiting for Resume, including after their last attached child leaves.

MUX children carry ordered bytes; neither callback nor wire-frame boundaries are preserved. Put UdpOverTcpClient before MuxClient and UdpOverTcpServer after MuxServer when carrying UDP datagrams. UdpOverTcp currently requires nonempty datagrams.

Stream delivery headroom and datagram framing​

ConnectionToPackets retains at most P + L pending TCP payload bytes per flow, where P is its configured max-pending-bytes and L is fixed at 1 MiB, independent of pool sizes, RAM profile and splice settings. This headroom bounds already-delivered input before Pause can take effect. Its 1,024-entry cap remains independent, and overflow is rejected before queue insertion. PacketsToConnection applies the same P + L bound on the return path; its acknowledgement records charge their original payload length until fully acknowledged and keeps production paused while that charge exceeds P, including when only ACK records remain. Both retain their independent entry caps. UDP has no such pending stream queue.

UdpOverTcpClient and UdpOverTcpServer carry UDP datagrams over a byte-stream carrier. Each datagram-facing Payload callback supplies exactly one datagram. The encoder prepends the two-byte length and hands off the whole owned buffer in one callback; input beyond 65,505 bytes is dropped intact. Source protocol flags never select a TCP payload mode. Mixed listeners route TCP separately.

Both nodes advertise splice support. Their fixed-header splice stream caches only the two-byte length, and each complete body is extracted into one owned buffer with onward padding. Datagram bodies remain eligible for private pipes; neither node sets an ordinary-read preference for UDP. UDP socket fallback belongs to the sender. Both initialize their framing state in Init and forward next Init immediately; the server sets the destination protocol to UDP first. There is no protocol-selection marker. Transport Est, Pause and Resume are forwarded promptly, without an encoder queue or pending control output.

Each receive stream drains complete admitted frames before checking its 4 MiB retained buffer charge, attempting beneficial compaction before refusal. The decoder accepts nonzero 16-bit wire lengths through 65,535 bytes. Its drain can stop only on an incomplete header or body, leaving at most 65,536 bytes including the two-byte header. Assert this derived bound instead of adding an unreachable overflow path. It does not bound allocation charge: a small suffix can retain a large backing buffer, so charge checks remain necessary. Parser reentry shares 2 MiB of retained logical bytes and 4 MiB of charge with the older stream. Complete frames in the synchronous dispatch continue after Pause; hard admission refusal, zero data length or Finish stops dispatch and closes a borrowed normal carrier through its owner. Neither node discards arbitrary stream bytes to resume parsing. Their fixed-header stream allocation follows the fatal heap-allocation policy.

Stream admission limits belong to the node and do not derive from the pool large tier, splice read target or actual pipe capacity. HttpProxyServer keeps a fixed 2 MiB of delivery headroom per direction alongside its configured working budget. An active large delivery publishes its unconsumed buffer while its prefix streams through working storage. Nested input appends behind that suffix and must fit the same direction's delivery headroom together with any deferred remainder; there is no separate reentry allowance. Raw fallback and CONNECT relay can hand off retained ordinary buffers directly, preserving padding and FIFO order even when the opposite direction occupies the working budget. When first-request authentication fallback is eligible, only the original complete header gets an extra immutable snapshot, bounded by max-header-bytes + 1. Wipe it on protected commitment or refusal. Selected replay enters the existing working/output budget before fallback Init; bodies and pipelined tails stay in the existing input/deferred slots. Admission must account for the active Payload suffix before any reentrant branch callback; snapshot storage is not another backlog allowance. ObfuscatorClient/Server retain at most 2 MiB plus one maximum wire record (65,540 bytes); their 65,535-byte record-body maximum remains separate. The packet/stream adapters above keep 1 MiB of extra delivery headroom. These values apply independently of RAM profile, platform support and misc.splice. Their entry limits, allocation checks and overflow/close behavior remain in force.

A pool tier is not a universal maximum callback size: frame assembly and other transforms can produce larger ordinary buffers even in chains without splice. Keep protocol record limits, pending-data budgets and allocation geometry distinct. Multiple valid records delivered together are not an oversized record; process them when possible while preserving bounded admission, FIFO and the exact node's flow-control role. Changing a pool or pipe target must not silently change these node policies.

HalfDuplexServer and HttpServer split waiting caps use bufferpoolGetWaitingBudgetBasis(), a separate immutable pool setting: 64 KiB in S1/S2 and 1 MiB in higher profiles. They keep 131070 * max(1, ceil(basis / 32768)): 262,140 bytes in S1/S2 and 4,194,240 bytes in higher profiles. Enlarging splice targets does not enlarge these waiting caps. Device pools inherit both the splice limit and waiting-budget basis from their worker pool; custom pools provide each explicitly.

ReverseServer also materializes complete splice deliveries in both directions while a half is unpaired. Handshake validation/removal, waiting FIFOs and cross-worker handshake replay use ordinary buffers. Handshake completion does not permit opaque retention while waiting; only pairing enables unchanged splice forwarding after startup FIFO replay. Materialization preserves onward padding; application retention follows the default 2 MiB / 1,024-buffer budget, independent of pool geometry and splice read sizes. PipeTunnel transfers paired opaque buffers across workers under its existing ownership and cancellation contract.

Payload length versus retained allocation charge​

sbufGetLength() measures buffer contents. That is the correct unit for ordinary queue byte counts, protocol thresholds, FIFO delivery statistics, and backpressure decisions, and buffer_queue_t.total_len intentionally keeps those logical payload semantics.

Do not treat that content length as a memory charge when a security or resource limit specifically promises to bound allocations retained by a live queue. An empty or one-byte sbuf_t can still retain its full capacity, left padding, header, and allocator alignment overhead. Memory-sensitive code that needs such a hard bound must keep a separate storage/resource charge. Mux's child, parent, detached and outgoing queues use the queue-capacity policy above; it deliberately does not bound physical kernel resources. It must not replace logical length in BufferQueue or in the incoming-child FlowResume threshold.

Use bufferqueueGetCharge() for an individual BufferQueue instead of maintaining a duplicate counter beside it. A whole-queue ownership transfer carries the budget binding and both cached totals; initialize the relinquished source as empty and unbound. Reserved pop keeps continued active retention charged. TCP adapters share one budget between the queue and active WIO reservation, retaining original capacity charge until completion or terminal callback detachment. Partial writes release logical bytes. Mux's children, parser and detached associations retain their existing policies.

sbufGetAllocationCharge(const sbuf_t *buf) is the canonical O(1) charge for an allocated buffer's own userspace storage. For ordinary buffers it returns sizeof(sbuf_t) + sbufGetTotalCapacity(buf) + kSbufAllocationAlignment; capacity already includes left padding. For splice wrappers it returns the header plus original left padding, SPLICE_BUFFER_STORAGE_SIZE, and alignment overhead. Splice capacity and length are logical, so they cannot measure wrapper storage. The helper works before pipe initialization, makes no allocation or syscall, and reads no payload bytes. Cursor movement, consumption, and prepending leave the charge unchanged unless storage is replaced. It excludes kernel pipe memory, allocator bookkeeping beyond explicit alignment overhead, and pool/queue storage. The separate queue-charge helpers use sbuf capacity for both representations. This does not change sbufGetAllocationCharge() or BufferQueue's logical-byte accounting.

bufferpoolQueryBestFit() predicts ordinary storage without allocating, consuming a cache entry, or making a reservation. It shares tier selection with bufferpoolGetBestFit() and reports aligned payload capacity, left padding, allocation charge, and whether a configured pool tier fits. Unrepresentable geometry returns false without changing the output. Predictions remain valid while pool geometry is unchanged. Mux uses this query for ordinary compaction and batch fallback reservations; compaction still requires a smaller pooled tier.

Linux RawSocket capture uses the same nonblocking worker FIFO admission as Linux TUN, with a shared 8 MiB allocation-charge and 512-packet budget. Each queue worker owns receive scratch and a bounded fragment-release batch outside that budget. A received record remains private until its Netlink verdict succeeds; only NF_DROP can lead to chain publication. On capacity refusal, keep the unadmitted output and disable reads until credit notification. Shared fragment storage uses detached allocations, never a different worker's buffer pool. Inactive startup/cleanup service uses coordinator-owned storage and NF_ACCEPT; it never dispatches into the session FIFO. Once capture activates, coordinator service during the remaining worker startup gate uses detached storage and NF_DROP, then admits packets through the same bounded FIFO. Capacity refusal retains the current output and stops that lane's reads. No tunnel callback runs on the coordinator; worker delivery begins after packet Init.

Windows, macOS and BSD TUN readers configure the session's byte/packet budget and worker FIFO before producer startup: 8 MiB of buffer allocation charge and 1,024 packets. deviceReaderDistributeBatchDroppingOverflow() distributes complete visible native TCP/UDP packets by flow and other complete packets using a session-local sequence. It retains preserved-fragment association and groups packets in stable worker order to fit both limits before nonblocking admission. A capacity refusal drops and recycles the unadmitted remainder; worker 0 and the external macOS/BSD readers never wait for another worker. An individual allocation larger than the budget is dropped too. Accepted work retains its reservation through delivery or cancellation, including closed generations. Reader staging is bounded by one batch plus the existing fragment limits. Windows/macOS batches contain at most 128 packets. BSD uses min(RAM_PROFILE, kMaxReadDistributeQueueSize) with a 512-packet internal cap; supported profiles allow at most 256 packets (server). Message metadata is bounded separately by the packet limit. These limits bound receive dispatch, not total process memory or downstream retention.

Linux TUN offload work reserves storage before transferring an aggregate, an assured complete ordinary TCP/UDP record, or ordinary checksum work to its selected receive-processing worker in either offload mode. Single-reader native TCP/UDP uses the same flow worker as ordinary input. The destination is retained across capacity retries and all segments of that record. Both modes prepare receive scratch, the capacity notifier and ordered worker queues before producer startup. Their session budget is 8 MiB of allocation charge and 512 packet slots; common setup must admit the largest retained ordinary work item. Each GSO work item charges its actual aggregate buffer allocation, its work context, and one maximum output-buffer allocation allowance; it reserves two packet slots for the retained aggregate and active output. The worker allocates and delivers one independent segment at a time, or delivers an assured aggregate intact on an eligible trusted pair. The output allowance stays reserved through every segment's required checksum work and TunDevice delivery, so the worker never waits for reader budget capacity. Downstream retention follows ordinary packet-chain ownership after delivery.

All non-segment ordinary work, including assured DATA_VALID and matching NEEDS_CSUM records, reserves one packet slot, its compact packet buffer's predicted allocation charge, and its private work context before allocation and copying from reader scratch. It needs no segmentation output allowance. Its reservation remains held through worker checksum completion and delivery (omitting checksum completion when assured), or settles on refusal, cancellation or stale generation. Scratch remains reader-owned and reusable for the next read.

Before allocating each segment, the worker checks the current ordinary best-fit charge against its prepaid allowance. If custom ordinary pool geometry no longer fits that allowance, it creates an exact-sized ordinary buffer with this chain's original sufficient padding instead.

Ordinary batches use the same per-worker FIFO and reserve their actual buffer allocation charges and packet slots before posting. Reader drain staging and fragment storage retain their separate existing bounds; session and FIFO-message metadata are bounded separately by the packet limit. The 8 MiB admission charge is not a total process-memory limit. Budget exhaustion makes the reader wait, retaining at most one unadmitted record in its current scratch buffer. Successful GSO admission transfers that buffer itself and replaces reader scratch without copying the aggregate payload. Non-segment admission copies the exact IP bytes and reuses reader scratch. Release each work reservation exactly once after completion, refusal, cancellation, or stale-generation discard. Posting a work item is not settlement, and cancellation must not borrow a device or worker buffer pool.

Buffers transferred to scheduled line tasks​

Calling lineScheduleTaskWithBuf() or lineScheduleDelayedTaskWithBuf() transfers the buffer on every result, including synchronous RejectedSettled. The caller must never recycle, destroy, or inspect it after the call. If the task executes, the task callback receives ownership. Otherwise the scheduler settles the buffer after optional cancellation notification: it returns the buffer to the line's pool only while cleanup runs in that owner event-worker context, including owner-bound teardown, and destroys it on a foreign or unbound cleanup thread.

The cancellation callback does not receive or own the buffer. It runs before internal buffer release, while the scheduler's physical line reference remains held, and must not attempt to infer buffer lifetime from the cancellation reason. A null cancellation callback suppresses only notification; line death, submission refusal, quiescence, and teardown still release the buffer exactly once. This makes memory safety independent of task progress.

In debug builds, BUFFER_WONT_BE_REUSED(x) swaps a buffer for a fresh duplicate and destroys the original, so any stale pointer to the old buffer is caught quickly. If you see it in reference tunnels, it marks "this buffer must not be touched again after this point."

A Complete Prepend Example​

Putting it together — a framing tunnel that prefixes a 2-byte big-endian length on the upstream path, having advertised .required_padding_left = 2 in node.c:

void myTunnelUpStreamPayload(tunnel_t *t, line_t *l, sbuf_t *buf)
{
const uint16_t payload_len = (uint16_t) sbufGetLength(buf);

// We advertised 2 bytes of left padding, so this is guaranteed to fit.
assert(sbufGetLeftCapacity(buf) >= sizeof(uint16_t));
sbufShiftLeft(buf, sizeof(uint16_t));
sbufWriteUI16(buf, payload_len); // (use the project's endianness helper as needed)

// Hand the buffer upstream; we no longer own it.
tunnelNextUpStreamPayload(t, l, buf);
}

On the downstream side the paired tunnel reads and strips the header with sbufReadUI16 + sbufShiftRight, then forwards with tunnelPrevDownStreamPayload.

Common Buffer Mistakes​

  • Prepending without advertising padding, then relying on left capacity that only exists in one chain layout.
  • Misusing sbufShiftLeft when left capacity is insufficient (the assert is your friend in debug; release builds corrupt memory).
  • Reusing or reading a buffer after passing it to a forwarding callback.
  • Leaking a buffer on an error/close path.
  • Using a different worker's pool than the line's own.
  • Treating sbufGetMaximumWriteableSize as "spare room beyond the payload" — it is room from the cursor, so subtract the current length first.

HttpProxyClient framing and retention​

Optional origin DNS precedes client Init. DomainResolver retains early ordinary, private-pipe and prefix-plus-pipe input under its own 2 MiB logical/2 MiB allocation charge FIFO bound, with no separate 1,024-entry cap. Client retention below starts after core Init; these are separate stages, not a shared entry cap or an aggregate 2 MiB connection-memory guarantee. Neither DNS nor helper insertion requires materializing body bytes or changing read preferences or splice capability.

Successful CONNECT acceptance releases response carry and the final header copy, clearing all borrowed parsed-header pointers before queued uploads or coalesced tunnel bytes can trigger callbacks. Pending queues, active buffer reservations and the idle/work timer remain live. HTTP mode retains its parsing storage for body framing and trailer validation until teardown.

HttpProxyClient advertises ten bytes of chunk-prefix padding. Its initial request uses a separate checked best-fit ordinary buffer with onward padding. A chunk's size prefix stays with its original body wrapper; an ordinary two-byte CRLF suffix is published before handing off the body and sent immediately afterward. Nested upload input waits behind that suffix. Pause/Resume relay transparently; the suffix and ready backlogs do not wait for Resume. Init, request/response readiness and reentrant dispatch still impose ordering barriers.

HttpProxyClient does not request ordinary reads by default: uploads and body-only response deliveries can preserve splice buffers. Preferences set by other nodes remain advisory; both ordinary and splice input are valid in every phase.

Ready CONNECT, fixed uploads, and response buffers wholly within an established fixed/EOF/chunk-data range can transfer intact. Inputs containing HTTP metadata or crossing a body boundary materialize completely into best-fit ordinary storage through sbufEnsureOrdinary(), after admission enforces the delivery bound. No splice wrapper enters the ordinary parser. This practical scope deliberately allows copies for mixed inputs and small chunks.

A delivery is at most 2 MiB. Necessary pending input per direction shares a buffer_budget_t bound of 2 MiB logical bytes, 2 MiB queue-capacity charge and 1,024 entries. Active retained parsing input holds a reservation under the same budget; consumption releases bytes before callbacks. The opposite direction has its own budget so full upload storage cannot stop response progress. Header and framing carry have explicit fixed bounds; they are not general body queues. Materialization may transiently allocate one additional complete ordinary input of at most 2 MiB payload with onward padding, independently of pool tier sizes. This is not a physical-memory or kernel-pipe capacity guarantee.


Continue to Part 4: Packet Lines & Packet Tunnels.

lwIP engine and shared buffer storage​

Production lwIP uses a fixed 2 MiB TCP send capacity and a fixed 4 MiB receive window per PCB. Receive-window scale 7 represents the full window when the peer negotiates scaling; peers without scaling retain the unscaled limit. These are protocol ceilings, not per-flow memory reservations. Payload storage comes from bounded shared pools as data is retained, so allocation pressure can limit a flow before its byte ceiling. Increasing these ceilings does not multiply the shared heap or segment pools by the flow count. The ordinary receive-pbuf pool reserves enough payload capacity for one full receive window process-wide.

PTC's default max-pending-bytes is also 2 MiB because it counts both queued payload and original deliveries awaiting full acknowledgement. Its fixed 1 MiB delivery headroom and separate 1,024-record limit still bound admission. CTP's pending budget counts only payload not yet accepted by lwIP; its default remains 256 KiB independently of the stack's send capacity.

PTC and CTP admit stream payload into a scaled TCP send buffer through successive 16-bit tcp_write() calls. That API's 65,535-byte limit is not backpressure: admission and FIFO retry continue while send capacity and allocation permit. Each bridge keeps the uncopied suffix at the head of its write FIFO. PTC also retains the original payload's ACK record charge until acknowledgement. A retryable failure after a successful prefix still flushes that prefix so acknowledgement can restore progress.

Worker engines share one bounded set of flow/payload pools, including PTC RX wrappers. Each engine caches at most 256 free objects per TCP-segment, pooled-heap size class and PTC RX-wrapper pool. A cache miss refills up to eight objects from the existing backing pool; it does not allocate another copy of that pool or increase its process-wide capacity. Warm checkout/return avoids the shared allocator mutex. The owner atomically detaches a free list before reading or changing it, then publishes the remaining list. Shared-pressure allocation can detach published lists from other engines, returning their free objects to the backing pool before retrying. A list briefly detached by an in-progress owner operation is published again before that operation returns. No pressure path traverses another engine's live PCBs or pbufs.

An entered engine may cache returned backing-pool storage regardless of which worker originally allocated it. An unscoped or unregistered free uses the shared allocator, without dereferencing a former owner's cache. Engine destruction unregisters and drains its caches under shared protection before releasing the engine; backing pools outlive all engines. Engine creation must not reset shared allocation metadata. Pool locks cover allocator metadata only; custom destructors and local protocol reclamation run after unlocking. Exhaustion still returns NULL; an engine may reclaim its own eligible protocol objects before refusal.

Pool used/max statistics include objects reserved in local caches as well as live objects; they describe storage checked out from the backing pool. Heap-byte statistics continue to count live requested bytes. pbuf reference protection, shared heap-byte statistics, ports, packet IDs and reassembly quotas keep their existing synchronization. Allocator overflow/sanity diagnostics and memp-available hooks use the uncached implementation so their per-operation checks remain intact.

The Unix SYS_ARCH_PROTECT guard uses the project's hybrid wmutex_t. Exclusive shared bootstrap initializes it before allocator use, and shared finalization destroys it after all engines and retained buffers have settled. Thread-local nesting acquires the mutex only on the outermost entry and releases it on the matching final exit. This protects shared metadata; it does not add a TCP/IP core lock around worker-local protocol execution.

Each active engine separately owns 17 timeout records and its control tables. This control overhead does not multiply TCP, UDP, listener, segment, pbuf or heap capacities. IPv4 and IPv6 reassembly retain separate process-wide pbuf allowances. PTC and CTP detached drain lists are worker-local, but each node retains its aggregate 4,096-object and 16 MiB allowance with exact reservation rollback.

Engine-owned pbuf chains remain on their owner until release. Cross-worker messages carry detached bytes and lookup identity, not permission to access foreign pbufs, PCBs or worker pools. Retained custom pbufs must settle before owner buffer pools disappear. Foreign cancellation destroys detached storage; it does not recycle into another worker's local pool.

PTC stages accepted TCP receive bytes in one owner-held sbuf while its deferred normal-line delivery is pending. The first receive can reuse a single, uniquely referenced PTC custom pbuf's sbuf from the same owner worker and pool. Its accepted payload range must lie inside that sbuf, both its original padding reservation and remaining headroom must satisfy the onward pool requirement, and its payload allocation must be no larger than the smallest fitting copy destination. An oversized pool tier retains the bounded copy fallback. Other inputs are copied; new allocations use the smallest fitting tier. Growth is geometric and staging is bounded by TCP_WND, including a dedicated fallback when a custom pool tier exceeds that bound. Growth briefly owns both old and new storage. The task owns a line reference, not this buffer; owner teardown releases staging after cancellation. Delivery detaches the sbuf before calling the next tunnel, preserving reentrant input and Finish ordering. One aggregated Payload retains the existing accepted-input Pause tolerance and returns or withholds exactly its byte count as TCP receive credit. Refused task submission releases copied staging and rolls back credit without consuming lwIP's pbuf. A reusable input remains unchanged until the owner task is admitted; then its sbuf transfers to staging and only the custom pbuf wrapper is freed. Stripping packet headers does not replace the original padding reservation.

PTC receive copies use pbufLargeCopyToPtr(), which copies at most the first packet's tot_len, skips empty spans, and stops before a queued next packet. The caller supplies nonoverlapping destination storage for that length. Each span below 64 bytes uses ordinary copying; larger spans use memoryCopyLarge(). Receive-buffer growth uses the same cutoff. Neither path assumes the TCP payload or append position retains the original IP packet's alignment.

PTC netif output copies exactly one packet's tot_len from its pbuf spans into one best-fit sbuf on the route owner worker, preserving chain padding. Empty spans are legal; a following packet in a pbuf queue is excluded. Delivery transfers that same sbuf. Deferred refusal/cancellation calls sbufDestroy() without tunnel, line, engine or pool access; accepted owner delivery may recycle a stopped output. No pbuf storage crosses the device callback boundary.

TUN output has one FIFO per event worker and uses that worker's existing buffer pool; there is no device writer pool. The device-wide packet allowance covers both queued buffers and the active flush batches across all workers. The queue limit is separate from the flush quantum: each deferred callback removes at most 64 packets and 256 KiB of allocation charge, leaving the next packet queued when it would exceed the quantum. A singleton exceeding that charge target still progresses ordinarily. No timer waits for more packets; remaining output schedules another deferred flush.

Linux trusted GSO records copy only a private IPv4/TCP header; iovecs borrow original payload spans until the complete write attempt and EINTR retries finish. No aggregate payload allocation/copy occurs. Every native batch callback settles all of its buffers through its event worker's pool. Cancellation marks the queue for discard; lifecycle queue drain destroys retained buffers without borrowing a worker pool. A refused tundeviceWrite() leaves the buffer with its caller; acceptance transfers it to writer settlement. Generation references keep late flush-token cleanup safe after device destruction; see Part 7.

Raw-device output also uses per-worker FIFOs and the existing worker buffer pools, with device-wide allowances of 131,072 packets on Linux and 256 on Windows, covering queued and active buffers. A native callback consumes only the prefix it reports; the helper retains the unsent suffix ahead of later FIFO entries, including across partial sends, interruptions and writable retries. Linux keeps up to 512 original buffers in an active batch. Each callback performs at most one nonblocking sendmmsg(); no aggregate payload allocation is introduced. Invalid or oversized input at a later position waits until the earlier valid prefix settles before it is dropped.

Windows overlapped output may borrow the first unsent packet while the kernel operation is pending. Neither completion notification nor foreign cancellation may recycle that buffer. The owner worker settles completed output through its own pool. During stop, native cancellation/completion and notification quiescence must finish before any retained packet is destroyed. Refused rawdeviceWrite() keeps caller ownership; accepted output settles through the writer even when a later retry is cancelled.