What is built, how it is proven, what it got wrong

This is the engineering record rather than the pitch. It covers the testing standard, the infrastructure PIGEON is checked against, and six defects found in PIGEON’s own code — including how each one surfaced, because how it was caught is the part that transfers.

The testing standard

Tests are standalone programs built against the core library. There is no test framework: each one is a main() with counted checks, so a failing suite prints the check that failed rather than a stack trace from inside somebody else’s harness. There are sixty-six of them in the main tree today, plus three for the legacy receive-side tool, and they are driven against real servers, real filesystems and real binaries rather than against mocks.

Exercise behaviour, not shapes

The rule exists because of three failures in this product’s own history that compiled, linked, passed review, and were caught only by running the thing:

  • A signing option silently signed nothing and exited 0. The guard the signing code sat behind was never true, so the code was unreachable — and unreachable code cannot report an error.
  • A plain copy reported success over a truncated file, with exit 0.
  • Archive mode on Windows reported completed having delivered zero files.

None of those would have been caught by a test that checked the shape of a result. All three are caught by a test that copies a real file and then looks at the destination.

Component tests miss seam bugs

The batch-engine suite drives the batch engine directly, so it could not see that the manifest written by the layer above it broke the next batch. That defect lived between two components, and finding it needed an end-to-end round trip through the command-line tool. When a bug can live in a seam, the answer is the end-to-end case, not another unit test on either side of it.

A test that cannot fail for the bug it targets is the test’s bug

This is treated as a defect class in its own right, and several have been found and fixed. A check written as … || !v.ok is always true, and once removed it showed that the wording it claimed to assert had never matched the real message. A test that counted destination files recursively counted the quarantine directory too, so a run that swept every delivered file aside scored the same as one that deleted nothing. A set of hostile-input cases was piped through a tool one platform does not have, so on that platform they could only ever have “passed” by feeding the reader something that was not the format at all.

A related one, because it cost real time twice: a suite whose assertion is “the command failed” needs separate proof that the command ran. A launcher error and a correct refusal both produce a non-zero exit, and one of them is not a result.

Falsify the test before trusting it

A new regression test is run against the unfixed code first and the failure count recorded. A test that passes against the code it was written to catch is not a test. That is why several of the defects below are quoted with a number beside them — 2 collisions in 32,000 names, 3,145,832 of 12,582,912 bytes, nine failures against the old behaviour. Those numbers are the test proving it can fail.

Skip honestly, and never quietly

A suite that cannot get the infrastructure it needs prints a skip and says which piece was missing. That is not the same as passing, and the distinction is enforced: a provisioning step for optional test infrastructure must not be able to fail the build, and a step that provisions something must gate on the artifact it produced rather than on a proxy for it. A cache key matching is not a usable library on disk; a recorded process id is not a server listening; an importable package is not the importable symbol the test actually needs. Each of those was, at some point, a step reporting success about a state that did not exist.

What it is tested against

Every protocol PIGEON speaks is checked against an implementation somebody else wrote, in both directions where both directions exist.

A real OpenSSH server
SFTP push and pull, remote listing, differential push, byte-range resume and server mode, against a real sshd with a real key exchange.
Real, independent TFTP implementations, both ways
PIGEON’s client pushes to a real atftpd and reads the files back byte-identical; a real tftp-hpa client performs GET, PUT and a manifest fetch against PIGEON’s own server. The second direction found something only an independent client could: that client defaults to text mode rather than binary. PIGEON’s server refuses it rather than silently translating line endings — which, for the binary firmware images this transport exists to carry, would be real and silent corruption.
A real FTPS server
Real explicit TLS, real certificates, including a deliberate check that a stale partial file is discarded rather than resumed from.
PIGEON’s own client against PIGEON’s own server
Not a hand-rolled test client, and not either side in isolation. That pairing found two real bugs immediately, both of which look correct when either side is examined alone: a development certificate with no subject alternative name, which verifies perfectly for a client connecting by the literal common name and fails for every client that connects to localhost or a loopback address; and a manifest handler that answered only at the document root, while the client always asks relative to whatever sub-path it was pointed at — so every pull from a real sub-path returned not-found.
Real NTFS volumes
Created and mounted for the purpose. The cheaper alternative — creating a file with a colon in its name on an ordinary Linux filesystem — produces a file with a colon in its name, not an alternate data stream, and a test built on it would have passed for entirely the wrong reason.
Real separate filesystems
Loopback images and in-memory volumes, so removable-media identity and same-drive detection are compared between two filesystems that genuinely are different rather than two paths assumed to be.
Native Windows hardware
Continuous integration builds and runs the suite on real Windows, with a real compiler hosted there, not by cross-compiling and emulating.

“Compiles, not proven” was a status we wrote down

Several Windows-only features carried exactly that status, in writing, in the engineering log, for as long as it was true. A compatibility layer on Linux was enough to show the code built and ran; it was not enough to show the feature worked, and in one case it actively said the opposite of the truth. Each was marked proven only when a real Windows machine settled it:

  • NTFS alternate data streams. A source file given two real streams, with their presence asserted before the transfer so the fixture could not be an assumption, and both read back byte-identical from the delivered copy afterward.
  • Access-control reproduction. An entry added to the source by well-known identifier rather than by name, so a machine in another language could not silently miss it; the destination checked beforehand to confirm it did not already carry that entry, so the test was able to fail; and the result read back from a separate process. The reason that last condition matters is the fifth defect below.
  • Volume identity. Two genuinely distinct volumes, with the difference asserted before the test ran — comparing a drive with itself passes without proving anything.
  • SFTP push and pull through the host’s own Windows OpenSSH client, including a file in a subdirectory. A flat, single-file tree never exercises the path-joining hazard this class of bug hides in, which is exactly how the same bug survived a clean cross-compile elsewhere.
  • The background service registering with the service manager, starting, running and stopping. Survival across a reboot is still not tested, and is not claimed.

Six defects, and how each one surfaced

Five of these are PIGEON’s own bugs, not a competitor’s. The sixth is not a bug at all — it is an episode that changed what this project accepts as evidence, which is why it sits with the others. They are here because a transfer tool asks to be trusted with the one thing its user cannot check by eye, and the honest way to earn that is to show the work rather than assert the result.

1. A hash cache that is allowed to miss one edit — and says so

File-sync tools have a long history of trusting file timestamps, and the reason is obvious: hashing a large tree costs real time, and size-plus-modification-time is free. It is also defeatable. Any cache keyed on size and timestamp can be handed a file whose content changed and whose timestamp did not.

PIGEON hashes by content and has no timestamp shortcut at all unless a profile opts into one. When it does, the cache carries two independent guards.

The first is the rule git uses against the same problem. A cached entry is trusted only when the file’s modification time is strictly older than the cache’s own, so an edit landing in the same second as the run that cached it is never trusted and the file is re-hashed. A later pass tightened the reference point: the threshold is the moment the scan started, not the moment the cache was written. A file edited during a long scan — after its hash was taken, before the cache was saved — would otherwise have a timestamp older than the cache file and be trusted.

The second guard exists because the first cannot be made sufficient. Content changed with the modification time deliberately restored is invisible to any rule founded on that timestamp. Not to a stricter one; to all of them. So there is a periodic full rescan, on a schedule the profile sets, that ignores the cache entirely.

The test suite demonstrates that miss on purpose. It makes a same-length edit, restores the file’s modification time bit-for-bit, and asserts that the run reports a cache hit and does not deliver the change — then forces the rescan due and shows the same edit being found. The note beside it says not to “fix” that section by tightening the first rule, because no timestamp rule can see that edit and the honest answer is the backstop.

And a real timestamp bug this work surfaced. C++17 has no portable conversion from a filesystem timestamp to a wall-clock one. The workaround is to sample both clocks and use the difference as an offset — and the original code re-sampled both, fresh, for every file on every scan. That quietly assumes the two clocks advance in lockstep at the same resolution. On one Windows toolchain they do not, so the same unmoved filesystem timestamp converted to two different whole-second values depending on precisely when each scan happened to sample the offset. A comparison of “same size, same timestamp, different content” — a test that restores the timestamp on purpose, to prove the difference engine is not fooled by it — failed because the two timestamps no longer compared equal, though the filesystem attribute had never moved. Each clock is now sampled once for the life of the process, so the conversion is a deterministic function of the raw timestamp and nothing else.

2. A temporary filename that grew past the limit

An upload handler that writes to a staging file and renames it into place on success is the ordinary way to avoid leaving a half-written file at the real destination. The obvious way to name that staging file is to extend the name the client sent.

That is a bug, and an unsubtle one once seen. Filesystems limit the length of a single path component — 255 bytes on both ext4 and NTFS — and the suffix is around forty characters. A filename already within forty bytes of the limit produces a staging name over it.

Reproduced rather than reasoned about: a 234-byte filename, comfortably legal, confirmed to write directly with an ordinary copy, was refused by PIGEON’s own embedded HTTPS server with a 500. Two upload handlers had the identical defect — the same suffix string, apparently copied from one file to the other.

The fix stops extending the client’s name at all. The staging name is derived from a hash of the destination path, placed in the destination’s own directory so the final rename is still same-filesystem and still atomic, and its length no longer depends on the real filename’s length. Every other staging convention in the codebase was then audited: the rest key off a small integer index or a fixed internal filename, so none of them can reach the limit by construction rather than by luck.

A second finding in the same helper, several passes later. The name it built was unique only because two threads rarely read the clock on the same tick. Measured, that was 15 collisions in 200,000 names. Two concurrent writers of one file then shared a staging name, both opened it truncating, and the losing writer’s own short-write check deleted the bytes the other had just staged — a name collision in a helper whose only job is to be unique, turning into the one outcome this product exists to prevent. A timestamp is not an identity: the name now also carries the process id and an atomic counter, which are unique by construction. The regression test was run against the unfixed code first and produced 2 collisions in 32,000.

3. The hole at the end of the file

Sparse-file support is a small feature. When a chunk of the source is entirely zero bytes, seek past it at the destination instead of writing it and let the filesystem leave a hole. A hole reads back as zeros, identical to what the source held there, so the existing verification needed no changes at all.

Except that seeking past the end of a stream, with no write after it, does not extend the file when the stream closes. It is an elementary fact about C++ streams, and exactly the one you walk into when the entire point of the feature is to seek instead of writing. A source file whose last chunks are all zeros would land short at the destination — silently, and while still hashing correctly for every byte it did contain. A truncated file that verifies for everything it holds is precisely the failure this product exists to prevent, arriving through the front door of a feature meant to be an optimisation.

The fix is one unconditional resize to the source’s real length after the write loop closes the file: a correct no-op in every ordinary case, and the actual fix when the trailing chunks were holes. One call covers both without tracking which happened.

The feature also cannot be proven by comparing hashes, because a fully allocated file and a genuinely sparse one hash identically — sparseness is a storage property invisible to content. The test reads the allocated block count instead. A realistically shaped 16 MB file — real content, a 16 MB run of zeros, real content again, rather than an all-zero file that would never exercise the interesting case — allocates 16,781,312 bytes copied normally and 266,240 bytes copied sparse, with both copies byte-for-byte identical to the source and both passing verification.

NTFS needs the file marked sparse-capable before an extension can become a real hole; without that mark the identical operation is zero-filled and fully allocated, which defeats the point quietly rather than loudly. A destination that cannot do it — FAT, many network shares — still receives the file byte-correct, just fully allocated, and the run records in its own receipt that the profile’s intent did not fully happen.

4. The move that never moved

A move is a copy, a verification, and then a deletion of the source. On Windows, PIGEON’s version never performed the third step. Not intermittently — every file, every run, for as long as the feature existed.

The cause is one line that was not there. The read handle the copy opened on the source was still open when the deletion was attempted a few lines later. On POSIX, unlinking a file that is still open is entirely ordinary; the directory entry detaches and the data survives until the last handle closes. Windows enforces sharing at the handle level, and an ordinary read stream does not ask for delete sharing, so every deletion was refused.

What makes it worth telling is the shape of the failure rather than the cause. The run reported completed. The correct content landed at the destination and verified. The deletion’s own failure path did exactly what it was designed to do for the legitimate case — a read-only source, a permissions problem — which is to log a warning and increment a counter. It simply happened to be doing that for every file on every run, so a feature an operator had deliberately switched on did nothing at all, in the one direction where nothing is the opposite of what was asked.

It was found the first time the suite ran on a native Windows machine rather than on Linux or under a compatibility layer. Two apparently unrelated failures in other suites pointed at the same root cause and confirmed it: the emptied source directory was never pruned, because nothing had emptied it; and in a hardlink test only the primary group member survived, because only the primary goes through the code path that holds the read handle open.

5. A verification that verified itself

Windows permission reproduction: read the source file’s owner, group and access-control list, and apply them to the destination. The system call returned success, so the first version trusted it.

A real end-to-end run showed otherwise. The call succeeded, the run reported no degradation, and the destination file’s actual permissions were the default the destination volume would have produced anyway. So a verification step was added, in keeping with how the rest of this product treats a library’s return value: re-read the destination’s security descriptor immediately afterward and compare it, field by field, against what was requested.

The same test then reported a clean match.

Taken alone, that reads as “the earlier result was a fluke and the code is fine.” It was not. A second, genuinely separate process — sharing no state, no handle and no cache with the one that had just run the copy — read the same file and saw the unchanged original. The change had never reached the real file on disk. The read-back had been answered by something the writing process could see and nothing else could.

The cause turned out not to be PIGEON’s code. The compatibility layer the test was running under answers that pair of calls consistently to the process that made them, and does not carry the change through to the real filesystem underneath — confirmed by reproducing it with a standalone program that shared no code with PIGEON at all, rather than inferred from the symptom. Which is precisely why it belongs in this list: what the episode changed is not a line of transfer code, it is the standard of evidence.

The generalisable part is the one worth keeping: a same-process “verify what you just set” check is not evidence that a change persisted. It looks exactly like a working implementation, because a working implementation answers identically. Only a reader with no relationship to the writer can tell the two apart. That requirement was written down as binding on anything built on the same pair of calls — and when a real Windows machine later checked the access-control half from a separate process, it passed.

The honest remainder, stated here as it is stated internally: the owner half of that check has no negative control. Nothing establishes that an ordinary copy would not already have produced the same owner, so it could pass without the code having set anything. Access-control reproduction on real Windows is confirmed. Owner reproduction is not contradicted and is not proven.

6. A corrupt state file failed — but failed vaguely

The incremental air-gap workflow keeps a small local record of what the far end is believed to hold. Both that record and a batch’s own header already refused to proceed when they would not parse, and that part was right: treating a corrupt record as “the far end holds nothing” would send a full batch where an incremental was intended, which is worse than stopping.

What was wrong was the answer given. Neither refusal set a typed error, so both fell through to a generic catch-all whose category said the problem was environmental and whose destination state said unknown. Both statements were false. A record that will not parse is a data-integrity problem, not a host problem; and in both cases the engine knows exactly what has happened to the destination at the moment it refuses, which is nothing. The write side refuses before the batch is built at all. The read side refuses while reading the header, before the destination is touched.

Two codes were added, one per call site rather than one shared between them — the two failures happen at different points of different workflows, and a technician reading a code off a form needs to know which — each carrying the destination state untouched.

Then making that word true turned up something else. The apply path wrote a delivery receipt unconditionally, including on a failure where nothing had been validated. For an unreadable header the entry list is empty by construction, so it wrote a receipt for zero files — which created the destination directory if it did not already exist and put a file in it. That contradicts exactly what “untouched” promises. It was found by writing the test rather than by reading the code: the first draft asserted the destination was untouched and failed immediately, against a real directory holding a real, empty receipt.

What PIGEON refuses to do

Refusals are a design output here, not an omission. Each of these is enforced in the engine and again in the editor, so a configuration that cannot be safe cannot be saved.

Move mode and mirror-delete cannot be combined, and there is no override

Move mode removes each source file once its delivery is verified. Mirror-delete removes a destination file the source no longer has. Set both on one profile and run it twice: the second run sees a source missing everything the first run correctly delivered and removed, cannot distinguish that from an operator deleting those files on purpose, and deletes the only remaining copy from the destination. That is not an exotic edge case — it is the guaranteed, deterministic outcome of running an ordinary move profile a second time, which is exactly what a profile meant to run repeatedly gets used for.

The combination is refused before either mode starts. Elsewhere PIGEON provides override flags for a technician who has checked that a particular risk does not apply to their situation. This one has none, because the danger is structural to what the two features each promise rather than something a force flag could responsibly hand back.

Mirror-delete only removes what this profile recorded delivering

Each profile keeps a record of every path it has verifiably delivered to that destination. A file the difference engine calls “deleted” that is not in that record — a file something else put there — is left in place, with a notice naming it. A record that exists but will not parse is treated exactly like an empty one, so nothing at all is deleted, and it is rebuilt from what that same run verified, so one further run repairs it with no intervention. A consequence worth stating because it looks like a bug and is not: the first time mirror-delete is switched on for a profile, it deletes nothing.

Where nobody can be asked, nothing is deleted

On the receive side of a one-way delivery there is no return channel, so a receiver cannot ask the sender whether a file it does not recognise was meant to go. Those files are moved into a dated quarantine directory inside the destination — a same-filesystem rename, so it is atomic and cannot leave a half-written file behind — rather than removed. One directory per delivery, so a technician can tell which delivery set a given file aside. The same applies to a batch an unattended receiver refuses: it is moved aside with its signature, never deleted, because a refused delivery is the only evidence of the refusal.

A one-way receiver refuses an unsigned batch

Under an ordinary profile an unsigned batch is applied with a loud, audited warning — there is a way to go back and ask. Under a one-way profile it is refused and quarantined, because there is not, and a drop directory on the receive side of a diode is exactly where a forged batch would be placed. Any profile can be set to require a signature outright.

Local state fails closed

A corrupt sequencing record, a corrupt delivery record, a header that will not parse: each refuses rather than assuming the benign reading. The benign reading is always the one that reopens the hole the record was keeping shut.

Scope lines

These are limits the product states rather than ones a reader has to discover.

Verification is an application-layer guarantee
It proves the right bytes reached the right destination path. It does not replace a one-way device’s own link-layer error correction, and over a genuinely one-way link it can detect a corrupted delivery but cannot correct one — there is nobody to ask for a resend. The two are separate controls and should be written into a security plan as two.
TFTP has no delete
RFC 1350 defines no delete operation at all, so there is no wire command to issue. Mirror-delete on a TFTP push records an explicit degradation naming the file left in place, rather than silently doing nothing or failing the run. The pull direction is unaffected — that is a local removal, and the wire protocol never enters into it.
Directory confinement is not available on Windows
PIGEON’s SFTP server mode runs the host’s own OpenSSH server, and Windows OpenSSH does not implement the chroot directive at all. On Windows that mode runs unconfined but still restricted to SFTP, and confinement is not claimed there. No amount of permission engineering closes it; it is a platform gap.
Block-level deltas are scoped to the hand-carried batch workflow
A block delta only saves real bytes when the delta itself is the thing crossing the bottleneck, which is what a batch on removable media is. For a live copy the patching API used produces a complete output stream regardless of how much of it came from the basis, so the bytes written to the destination are the same as a whole-file copy; a real saving there would need in-place patching of a live destination file, and there is no proven-safe pattern for that here yet.
Archive mode is not throttled
The bandwidth cap applies to local copy, differential sync and all four remote transports. Archive mode writes and extracts a single compressed stream and does not pace it. A run that asked for a cap and did not get one records that in its own degradation notices rather than leaving it to be noticed.
Archive mode and the batch workflow do not run over the remote transports
Both need something at the far end to act on what arrives — extract an archive, apply a batch’s difference — and none of SFTP, HTTPS, FTPS or TFTP has that. Building it would mean designing a new wire protocol, not filling in a missing code path, so it is a stated scope line rather than a backlog item.
Drive identity is volume-level
The check compares volume identity, which is what it is for. Two volumes on one physical disk compare as two different volumes.
Hardlink preservation applies to plain local copy
Grouping requires every member of a hardlink group to be seen in one pass. Differential sync selects a subset by design, so the precondition does not hold and the feature is deliberately not wired in there.
Byte-range resume covers three of four directions
An interrupted single file resumes from its last delivered byte over SFTP in both directions, FTPS in both directions, and HTTPS pull. An HTTPS push always sends the whole file: a plain HTTP upload has no partial-append behaviour that can be relied on across arbitrary servers, and PIGEON’s own server stages an upload to a temporary file and renames it only on success, so there is no partial content at the destination path to resume from in the first place.
The service survives a logoff; a reboot is untested
Registration, automatic-start configuration, start, run and stop are proven on real Windows. Survival across a reboot is not tested and is not claimed.

And what PIGEON is not

Not a content filter or sanitiser. PIGEON inspects no content. There is a hook that runs an external, accredited tool over every file before a byte moves and refuses the run on a non-zero exit — that is an integration seam, and describing it as PIGEON sanitising anything would put an unaccredited filter in an accredited one’s place.

Not a release authority. The optional content-authorization gate verifies a detached signature over every file against a trust bundle a technician placed by hand, and refuses the whole run before a byte moves if any file lacks one. It checks that a release decision was made and signed. It does not make one, and does not confer the authority to.

Not an authorization to operate. PIGEON gives assessors records, signatures and documentation to work with. It does not grant approval and cannot.

Tamper-evident, not tamper-proof — and precisely so. The audit chain detects a record edited, removed or reordered within the log. It cannot, by itself, detect records chopped off the end; that is why every delivery receipt carries a reference to the audit record that opened its run, so a technician holding a receipt can prove the log still contains the run it came from. Neither mechanism stops someone with write access replacing a file wholesale. Host controls still matter.

Naming an algorithm is not a validation claim. The algorithms PIGEON signs with are published standards. That is a property of the algorithm and says nothing about whether any particular implementation of it has been independently validated. PIGEON is not itself a validated cryptographic module and does not claim to be.