Refuse a multicast stream that is not the one this partition asked for - #178
Merged
Merged
Conversation
udpcast carries no metadata, so a stream is bound to a partition by nothing but its position in the sequence: the server chains one udp-sender per image file on a shared portbase, FOS opens one udp-receiver per file it expects, and the Nth receiver gets the Nth stream. A client that reboots mid-session -- or whose receiver opens after a sender has already stopped waiting for it -- therefore lands one stream out of step, and every partition after that is restored from the wrong image. partclone objects only when the target partition is SMALLER than the source; when it is larger the wrong filesystem is written and the deploy reports success. fogproject #1742 records a run where partition 3's image was caught on its way onto partition 2 (105 MB target, 135 MB source) only after partition 2's had already gone silently onto partition 1 (300 MB target). The server now introduces every stream with a fixed 128-byte record naming the image file it carries. writeImage reads it off the receiver before the decompressor is started -- so a refusal leaves the target untouched -- and compares it against the file it was asked for. That argument was already being passed in and thrown away on the multicast path; it is now the assertion. Three details are forced rather than chosen. The record is fixed width because a count of bytes is the only thing a pipe lets you read without consuming payload. It is read with bs=1 because a pipe may return a short read and over-reading eats the front of the image. And the header is only stripped when getversion.php?caps=1 advertises mcstreamid, because taking 128 bytes off a server that does not prepend them would corrupt every deploy -- the call is checked before its answer is read, so an unreachable server does not read as "no header" (the GH-1266 shape, same as the mclvm probe beside it). The name is the basename, except that a stream carrying several files is named by the stem they share, which is what the client's own glob collapses to. So d1p1.img* and sys.img.* both become the stem while rec.img.000 and rec.img.001 keep their names -- the sys/rec asymmetry is FOS's own and the server already mirrors it when deciding what to concatenate. Both sides reach the same string from their own layout; neither counts the other's streams. tests/checks/multicast-stream-identity.sh greps the wiring, because the ordering of "open the receiver, read the header, only then restore" is what makes a refusal safe, and executes the real function against synthetic headers for the naming rule, because that rule is not uniform. Seven mutations -- the call removed, the check moved after the payload starts flowing, each of the two suffix strips dropped, the comparison neutered, one byte short on the read, the capability guard removed -- each fail it. Writing that harness found a defect in the check itself: the right side of [[ != ]] is a pattern, so an unstripped d1p1.img* matched the stem it was meant to be compared against. Quoted. Verified against real udpcast on a loopback, driving the server's own emitted command and this function: a flat partition asked for by name and a split one asked for by glob both accepted with the payload byte-identical by sha256 after the header, and partition 3's stream offered to partition 2 refused. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A767exFmz6sQUcuZofqE1V
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Client half of the fix for FOGProject/fogproject#1742. Pairs with the
server change in fogproject (link below) — see Ordering at the end.
The bug
udpcast carries no metadata. A stream is bound to a partition by nothing but
its position: the server chains one
udp-senderper image file on a sharedportbase, FOS opens one
udp-receiverper file it expects, and the Nthreceiver gets the Nth stream.
So a client that reboots mid-session — or whose receiver opens after a sender
has already stopped waiting for it — lands one stream out of step, and every
partition after that is restored from the wrong image.
partclone objects only when the target partition is smaller than the
source. In the reported run partition 3's image (135 MB MSR) was caught on
its way onto partition 2 (105 MB EFI) — but only after partition 2's image
had already gone onto partition 1 with no error at all, because that target
was 300 MB. A larger target means the wrong filesystem is written and the
deploy reports success.
What this does
writeImagereads a fixed 128-byte header off the receiver, before thedecompressor is started, and refuses a stream that is not the file it was
asked for. A refusal therefore leaves the target partition untouched.
The expected name is
$file— the argumentwriteImagewas already beinghanded and discarding on the multicast path. It is now the assertion, and
the
Args Passed:line in an error stops being decorative.Three details are forced rather than chosen:
read without consuming payload.
bs=1, because a pipe may return a short read and over-reading eats thefront of the image.
mcstreamidviagetversion.php?caps=1— taking 128 bytes off a server that does notprepend them would corrupt every deploy. The call is checked before its
answer is read, so an unreachable server does not read as "no header"
(the GH-1266 shape, same as the
mclvmprobe beside it).Naming
The id is the basename, except a stream carrying several files is named by
the stem they share — which is exactly what the client's own glob collapses
to:
d1p2.imgd1p2.imgd1p1.img*d1p1.imgsys.img.*— one partitionsys.img.*sys.imgrec.img.000/.001— two partitionsrec.img.000The sys/rec asymmetry is FOS's own and the server already mirrors it when
deciding what to concatenate (#897). Both sides reach the same string from
their own layout, so neither re-derives the other's ordinal — the
re-derivation ADR-0007 argued against.
Verification
tests/checks/multicast-stream-identity.sh— 17 checks. It greps the wiring,because the ordering of "open the receiver, read the header, only then
restore" is what makes a refusal safe and there is nothing to execute that
would show it; and it executes the real function against synthetic headers
for the naming rule, because that rule is not uniform.
Seven mutations, each failing it: the call removed; the check moved after the
payload starts flowing; each of the two suffix strips dropped; the comparison
neutered; one byte short on the read; the capability guard removed.
Writing that harness found a defect in the check itself — the right side of
[[ != ]]is a pattern, so an unstrippedd1p1.img*matched the stem itwas meant to be compared against. Quoted, and the mutation that exposed it
now fails two checks instead of one.
Against real udpcast, driving the server's own emitted command and this
function over a loopback:
Full FOS suite: 21 passed, 0 failed.
Ordering
The server prepends the header unconditionally, so this needs to ship with
the server change, not after it. An old FOS against a new server feeds 128
bytes into the decompressor and fails there — a clean failure, not
corruption — and in practice FOS is served by the server it is talking to.
Rationale, alternatives (per-stream ports, and why the port budget and
ADR-0007 both rule them out) and the known gaps are in
docs/adr/0018-multicast-streams-identify-themselves.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01A767exFmz6sQUcuZofqE1V