Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Compression on the ssh stream

-compress asks an ssh:// far side for LZ4 on the data payloads (reads, writes, streamed segments, delta literals; never metadata or holes, which are already small). It is agreed in HELLO and applied one block at a time: each payload carries a flag saying whether it is raw or compressed; a block is probed on its first 64 KiB and goes raw without the rest being tried when the probe does not shrink below nine tenths; after eight such blocks in a row the next sixty-four go raw with no probe, then one is probed again, and if it does not pay the pause repeats at once. So text costs a compression pass per block and shrinks, and a tree of already-compressed data settles at one 64 KiB probe per sixty-five blocks and crosses at its own size. The codec is the LZ4 block format in this crate, so there is nothing to install on either side, and a serve from before this option answers the request with a no and the run proceeds uncompressed. -loglevel DEBUG says which of the two happened.

-compress.codec deflate swaps in the DEFLATE encoder this crate already carries for .tar.gz, and -compress.level 1..9 chooses how hard it works on both sides. On 4.5 MB of log-shaped text pulled through a counting stand-in, the serve sent 793,848 bytes at level 1 and 599,604 at level 9.

-compress.codec zstd swaps in a zstd encoder and decoder written for this crate, again with nothing to install: frames carry Huffman-coded literals and FSE-coded sequences and are read by zstd -d, and the decoder reads raw, RLE, Huffman and treeless literals, one bitstream or four, predefined / RLE / FSE-compressed / repeated sequence tables, and the three repeat-offset codes. Interop is asserted in both directions against /usr/bin/zstd: five corpora at three levels, plus a boundary sweep over the lengths where the format changes shape (0, 1, the 1 KiB single-stream/four-stream literals split, either side of the 128 KiB block edge; text, single-byte runs, incompressible noise, skewed wide alphabets, and blocks whose literals are nearly one byte) with and without a frame checksum, at levels -3 and -19 – 280 reference frames in the sweep alone. The frame’s XXH64 content checksum is both written and verified: the reference recomputes it on every frame this encoder produces, and refuses one whose content has been altered. The skewed corpora are there for a measured reason – without them the sweep never once entered the FSE-coded Huffman-weight decoder, so the arms said nothing about it. Three more branches were at zero when somebody finally counted – treeless literals, single-stream Huffman and repeat-mode sequence tables, all of which parse attacker-supplied bytes and none of which any reference frame had ever reached. A large skewed corpus (several blocks in one frame, so the reference starts reusing trees and tables) and two small ones (a literals section under 1 KiB, the only size the single-stream form can describe) fix it, and the sweep now fails if any of the four stops being exercised.

Concatenated frames used to be silent data loss. Given two frames in one buffer this decoder returned the first and stopped, with no error, where zstd -d returns both – measured at 320 bytes against 700. Every frame is decoded now, bytes that are not a frame are a refusal rather than a shrug, and skippable frames are skipped in all five arrangements the reference accepts them in (first, middle, last, two adjacent, and an empty payload).

A frame that names a dictionary is refused. Stepping over the header’s Dictionary_ID is the tempting shortcut, and it is silent tolerance: the blocks after it decode, the checksum over them matches, and the decoder returns bytes on an input zstd -d will not touch – measured, the reference answers Decoding error (36) : Dictionary mismatch and zero bytes where the shortcut returned 1,024. Same class as the multi-frame defect. A hostile corpus does not catch it on its own, because its bit flips do set the dictionary flag but never insert the ID, which shifts every later field and makes both decoders refuse for unrelated reasons; a well-formed dictionary frame has to be built, and is, at all three ID widths. What is still not detectable here: a dictionary used with its ID omitted, which the format permits and which no header distinguishes from an ordinary frame – the reference cannot tell either.

A declared window over 128 MiB is refused, and that was laxness until it was measured from the other direction. Every check here had been asking what this decoder refuses that zstd -d reads. Asking the inverse – what does the REFERENCE refuse that we read – found a frame from zstd --ultra -22 --long=31 that decoded here and got Window size larger than maximum : 2147483648 > 134217728 there. The boundary is measured on both sides: a hand-built single-segment frame declaring exactly 1<<27 decodes in both, and one declaring 200,000,000 is refused by both. What it costs is said rather than hidden: the reference’s cap is a default, so zstd -d --long=31 reads frames this now refuses. That trade is made this way round because the peer running zstd -d with no flags is the common case. It costs this codec’s own output nothing – a wire frame is at most 16 MiB. Hostile input is checked against the reference too, not just for panics: over 4,710 corrupted, truncated and random inputs, 129 are accepted and byte-identical to the reference, none are accepted only by us, and four are refused only by us – all four because RFC 8878 reserves the low two bits of Symbol_Compression_Modes and libzstd ignores them. A refusal the reference disagrees with fails that arm unless it names a rule like that one. Accepting where the reference refuses fails it too, and that zero is a measurement rather than a counter nobody has seen move: removing the dictionary refusal moves it from 0 to 3, so the zero means the corpus contains inputs that would expose laxness and none of them do. A planted case would not serve: a frame with trailing bytes that the decoder read to the frame end looks like one, but that behavior is a defect, and fixing it leaves such a counter with no subject at all. The same corpus is required to reach eight named conformance refusals (it reaches 24 distinct ones in total); a check that stops firing fails by name, which is how this codec keeps finding code that was argued about rather than run. Pushing the same 4.5 MB through the counting stand-in: plain ~4,537,400 bytes, lz4 ~1,118,400, zstd ~605,000, deflate ~602,600. (The last digits move by a few bytes between runs because the count includes the request paths, and the scratch directory’s name changes; the comparison is stable, the units digit is not.)

Getting there was encoder work rather than format work, and each step was measured on this same arm: lazy matching 960,872 -> 856,164, FSE sequence tables built from the block’s own symbol counts 856,164 -> 630,260, repeat offsets 630,260 -> 605,008. A deeper match chain was tried and reverted: 64 instead of 32 doubles the search and bought 0.16%.