91b459657e40bcf58b7971df94487df2e227144a
The multi-threaded pipeline introduced in 75afadb had two related defects
flagged by external review:
1. The writer's reorder buffer was unbounded. `ordered_writer` accepted
a `_cap` parameter that was documented as the in-flight bound but
was never read. The writer drained `done_rx` eagerly into a
`BTreeMap`, so neither the bounded job channel nor the bounded done
channel ever exerted backpressure on the reader. A slow or stuck
worker would let the writer accumulate every subsequent chunk in
`pending` until the system ran out of memory. With adversarial input
this is a memory-exhaustion vector; with merely uneven workers it
silently violated the documented memory ceiling.
2. The pipeline did not fail fast. When a worker hit an AEAD
authentication failure it returned `Err`, dropped its channel
clones, and exited — but the other workers, the reader, and the
writer kept running until natural EOF. On a tampered N-byte file
we burned full I/O plus (T-1)/T of the AEAD CPU before surfacing
the error. Combined with (1) this also stretched the window in
which `pending` could grow.
Both issues are addressed by a single rewrite of `pipeline.rs`:
- A bounded "permit" channel pre-filled with `in_flight_capacity`
`()` tokens. The reader acquires one before sending each job; the
writer releases one after flushing the corresponding chunk in
order. Total in-flight chunks (queued jobs + in-progress at
workers + pending in the reorder map) is now hard-capped at
`4 * threads`, with the writer in lockstep with the actual disk
write rather than ahead of it.
- An `Arc<AtomicBool> cancel` flag that workers set on AEAD failure.
Workers check it at the top of their loop and drain remaining
queued jobs without doing AEAD work. The reader checks it before
each new chunk, so a tampered chunk causes the reader to stop
within the in-flight window rather than after EOF.
The reader uses `permit_rx.recv_timeout(50ms)` rather than a blocking
`recv` so it can poll the cancel flag even when the rest of the
pipeline has quiesced. Without this, a 3-way deadlock is possible:
worker errors after all permits are out, the writer is blocked on a
missing-counter chunk that will never arrive, the other workers are
idle on `jobs_rx`, and the reader is blocked on `permit_rx`. The
50 ms wakeup is well below typical user-perceptible latency and only
runs when the pipeline is otherwise idle, so its cost is negligible.
While rewriting I also collapsed `encrypt_parallel`/`decrypt_parallel`
onto a shared `run_pipeline` helper parameterised by an `is_encrypt`
bool — the two functions previously duplicated ~150 lines of channel
plumbing for a one-line difference (`encrypt_in_place` vs
`decrypt_in_place`). Same for the reorder writer: a single
`ordered_writer` now returns `(OutSink, u64)`, and encrypt simply
ignores the byte count (decrypt uses it for the length cross-check).
Removed the stale "wrapping_add" on the in-order counter — wrapping
here would mask a real bug since `bump_counter` already rejects
overflow upstream — and corrected the per-thread memory estimate in
the module-level doc to match the new bounded model.
The job-channel capacity (`channel_capacity = 2 * threads`) is left
unchanged. The new permit cap (`4 * threads`) is deliberately larger
so out-of-order completion has slack; if the gap is ever exhausted
the only consequence is reader backpressure, never unbounded growth.
Test plan:
- `cargo test` still passes the full 28-test integration suite,
including `parallel_and_serial_outputs_round_trip` (proves the
refactored unified pipeline produces bit-identical output to the
serial path) and `rejects_tampered_ciphertext` (still surfaces
the AEAD error, now via the cancel path).
- Manual fail-fast probe: 200 MiB random plaintext, encrypt with
`-j 8`, flip a byte at offset 2000 (inside chunk 0), decrypt
with `-j 8`. Errors in ~2 ms, vs ~28 ms for a clean decrypt of
the same file — confirming the reader stopped within the
in-flight window rather than draining the whole input.
- The `ordered_writer` cancel deadlock case is hit organically by
the same probe: chunk 0 fails authentication, no further
counter-0 chunk ever arrives, but the reader exits via the
50 ms cancel poll and the rest of the pipeline drains.
Refs: external review (P2 / Gemini #1, Gemini #2, GLM51 #2/#7/#8).
fcry - [f]ile[cry]pt
A file en-/decryption tool for easy use.
Currently fcry uses ChaCha20Poly1305 (RFC 8439) as AEAD cipher provided by the chacha20poly1305 crate.
Status
Currently fcry is not thoroughly tested and in early stages of development.
There is a chance, that something is broken as of now.
Encryption seems to work, but due to a possible lack of understanding of some underlying methods
(or misinterpretation) it could theoretically be not effective at all.
See TODO.md for further information.
Description
Languages
Rust
100%