chore: release v0.6.41 — ship pipeline auto-commit - #624
Merged
Merged
Conversation
…rlapped path A production MFT read hung for four hours on a fragmented 14 GB table (73 extents) and was only ended by an external supervisor at its hard ceiling. Zero bytes read, zero CPU, no error, nothing logged. The hang sat immediately after "MFT extents bootstrapped from FRS 0 $DATA runlist (broker path)", in `get_mft_bitmap_internal`, which is the very next step and is silent unless the verbose flag is set. That function seeked and read on `self.handle` with `SetFilePointerEx` plus a NULL-`lpOverlapped` `ReadFile`. On the broker path `self.handle` is a `DuplicateHandle` copy of the Access Broker's `FILE_FLAG_OVERLAPPED` volume handle, which has no synchronous file pointer: the seek is meaningless and the read is issued asynchronously with nowhere to report completion, so the call never returns and never fails. The only error handling present was a graceful fallback on `Err`, and a hang is not an `Err`. `read_boot_sector` carried the identical pattern and is fixed with it. Both now use `read_handle_at`, the overlapped-offset primitive that already carries a 30 s completion deadline and a bounded abort retry, and which is correct on a synchronous handle too (the offset in the OVERLAPPED is honoured directly). This is the third site in this crate to need that remedy after the `$UpCase` read, so the reasoning is recorded at both call sites rather than in a commit message alone. Sparse extents are now skipped rather than read from a negative LCN, and a negative byte offset falls back to the all-valid bitmap instead of being cast into a nonsense position.
…orever Four IOCP readers waited on `GetQueuedCompletionStatus` with `u32::MAX` (INFINITE) and, when the call failed, logged a warning and `continue`d without clearing or re-issuing the failed slot. The in-flight count then drained toward zero while `completed_count < total_io_ops` stayed true, so the next wait parked in the kernel on a completion port with no pending I/O. No CPU, no bytes, no error, no end. That is the same signature as the four-hour production hang fixed in the previous commit, and the 30 s completion deadline that exists for exactly this reason had only ever been applied to one of the five readers. All four now poll at `IOCP_WAIT_POLL_INTERVAL_MS` and fail with `wait_deadline_exceeded` once `IOCP_WAIT_COMPLETION_DEADLINE` passes with no completion at all, matching `to_index.rs`. A genuine wait failure now returns `classify_wait_error_code` rather than being swallowed. `iocp_wait_for_completion` additionally changes shape from `Option<..>` to `Result<..>`. Returning `None` made the caller report a successful no-op completion, which both stalled the loop and, had it ever unwedged, would have produced a silently truncated index. A completion that matches no in-flight slot is likewise an error now, not a shrug.
One production `uffsd.log` reached 305 MB, dominated by an INFO line reading "MFT is fragmented" emitted roughly seventy times a minute. `MftExtentMap::new` logged the fragmentation summary, and the USN journal poll rebuilds an extent map on every tick that carries a created or renamed record (`usn::read_targeted_frs_records`, 500 ms cadence per drive). The result was self-feeding: writing the log line created a USN record, which triggered a poll, which rebuilt the map and wrote the line again. Constructors should not log. `new` is now silent and `const`, and the layout summary moves to an explicit `log_layout()` that the ten one-shot cold-load, capture and benchmark call sites invoke. The two hot paths — the journal poll and `lcn_resolve::for_each_record`, both driven by the same 500 ms tick — deliberately do not. No diagnostic is lost: a cold drive load still reports its fragmentation exactly once, which is the moment the information is actually wanted.
An MCP `uffs_search` sat silent for thirty minutes. The daemon never logged the request, neither side reported a timeout, and an identical call a moment later succeeded. `send_request` wrapped each individual `read_line` in a five-minute timeout, re-armed on every iteration of the notification-routing loop. The daemon broadcasts a `StatsHeartbeat` notification to every connected client about every thirty seconds, and each one reset the clock. A request the daemon never answered therefore waited forever while the code read as thoroughly timed-out. The deadline is now absolute: computed once before the first byte goes out and enforced with `timeout_at`, so routing a notification cannot buy the response more time. The writes are bounded by the same deadline — they had none at all, and on Windows a full 64 KB pipe buffer blocks the caller indefinitely when the daemon stops draining. Budget stays 300 s and is overridable through the same `UFFS_CLIENT_TIMEOUT_SECS` variable the synchronous client honours, including `0` to disable it under a debugger. It is deliberately more generous than the sync client's 60 s: this is the path that waits while a cold drive warms. Two regression tests drive an in-memory duplex on a paused clock, so 300 virtual seconds cost no wall time: one asserts that a heartbeating daemon which never answers still trips the deadline, the other that a response arriving after notifications is returned normally.
On 2026-09-04 an unrelated automation agent ran `Stop-Process -Name uffs-broker -Force` while clearing a stuck scheduled task. The broker exited with 0xFFFFFFFF, the SCM logged event 7034 and took no action, and the box ran without a broker for about seven hours until a human restarted it by hand. Every non-elevated MFT read failed for that whole window. Three separate gaps made a one-second problem last seven hours. The service had no recovery actions, so the SCM's response to a killed broker was to write an event-log line and stop. `--install` now configures a restart ladder (5 s, 10 s, 60 s, then give up) with a 24-hour failure-count reset, plus the failure flag — without the flag, recovery applies only to an unexpected termination and a reported non-zero exit is treated as a deliberate stop. A failure to set them is reported but not fatal: an installed, running broker with no ladder beats no broker. A serve loop that returned `Err` was logged and then reported to the SCM as a clean stop, so even a configured ladder would not have fired. It now reports `ERROR_SERVICE_SPECIFIC_ERROR`, which is what actually gets the broker restarted. The service kept no log at all: `init_tracing` writes to stdout and a service has none, so the broker's own account of the incident was written nowhere and the timeline had to be rebuilt from the Windows Security log. The SCM path now logs to `<log-dir>/uffs-broker.log` with daily rotation and seven files of retention. `--repair` applies the ladder to an already-installed service in place. Reinstalling would mean a stop/delete/create cycle, and a window with no broker at all, purely to change two SCM settings. The usage banner and the `--self-test-vss` argument lookup move to `broker/cli.rs` to keep `broker.rs` under the 800-LOC ceiling.
Both `uffsd` and `uffsmcp` opened their log with `tracing_appender::rolling::never`, which appends forever with no size cap, no rotation and no retention. One production `uffsd.log` reached 305 MB. The dominant source of that volume is fixed at its root in this same series, but an unbounded log is a standing hazard independent of what is writing to it: these are processes that run for weeks and nothing ever truncates them. Rotation is not automatic in `tracing` — it is an opt-in, and so is pruning: `Rotation::DAILY` alone still keeps every file ever written, so `max_log_files` is a second, separate opt-in. Both now rotate daily and keep seven files. The caller's directory and file stem are preserved, so `--log-file /var/log/uffsd.log` becomes `/var/log/uffsd.<date>.log`. A builder failure degrades to stdout logging rather than killing a detached daemon at startup, matching the existing treatment of an uncreatable log directory.
bitflags 2.13.1 -> 2.13.2, clap 4.6.6 -> 4.6.7, crossbeam-channel 0.5.16 -> 0.5.17, rmcp 3.1.4 -> 3.4.0, smallvec 1.15.2 -> 1.16.1, toml 1.1.4 -> 1.1.6, uuid 1.26.0 -> 1.26.1. crossbeam-channel is the one worth calling out: 0.5.17 fixes undefined behaviour in the bounded channel when a `SelectedOperation` is leaked, plus an overflow in bounded-channel initialization. UFFS uses bounded crossbeam channels in the MFT reader pipeline, so this is wanted on its merits rather than for currency. rmcp 3.4.0 deprecates the `ServerInfo` alias in favour of `ServerConfig`; the handler is migrated rather than having the warning suppressed. zerocopy stays at 0.8.56. 0.8.57 shipped on 2026-09-08 but does not carry the fix for the `clippy::empty_enums` false positive on its generated derive code — verified by inspecting `const_block()` in the published `zerocopy-derive-0.8.57` source, whose emitted allow-list still lacks the lint. Upstream google/zerocopy#3414 and its proposed fix are both still open. Detail is recorded on issue #617, including the new finding that the lint has moved from `clippy::pedantic` to `clippy::nursery`, which the workspace denies just the same. Supply chain: `cargo vet regenerate imports` picked up upstream audits covering clap, clap_builder, clap_derive and toml, and retired the `shlex` and `simdutf8` exemptions. The remaining six deltas are audited here from the real diffs rather than waved through. Notably the three lines a grep flags as new `unsafe` in smallvec are the pre-existing `MaybeUninit::uninit().assume_init()` block re-wrapped by rustfmt, with its SAFETY comment intact, and the only `std::fs`/`process`/`env` additions in the 6731-line rmcp diff are five lines in a Unix-socket test fixture. Exemptions fall from 273 to 167.
`uffs-gen-workflow --check` validated `pr-fast.yml` structurally and nothing else. The release, preview, nightly, tier-2 and dependabot workflows are hand-written and were outside every check — which is exactly where they drift, because they share four things by copy: action pins, toolchain versions, per-target RUSTFLAGS, and nextest profile names. The gate gains four properties over every `*.yml` under `.github/workflows`, read-only like the rest of the tool: 5. one SHA per action across all workflows; 6. `ziglang` / `cargo-zigbuild` versions matched against a new `[toolchain]` table in `gates.toml`; 7. matrix rows and job-level `RUSTFLAGS` matched against `[[target]]` rows, with `target-cpu=native` refused outside comments; 8. every `--profile` on a `nextest run|archive` command resolved against `.config/nextest.toml`. The two manifest tables sit before the first `[[gate]]`, where `check_gates_drift.sh`'s awk resets on any table header and so ignores them. No new gate, no hook regeneration, no `pr-fast.yml` change: the existing `workflow-drift` gate simply sees more. First run over the real tree is clean, and the two known false-positive shapes stay quiet: comments that forbid `target-cpu=native`, and `rustup toolchain install --profile minimal`. So does this repo's `cargo build --profile ship`, since property 8 is scoped to nextest commands. Ported from the docenta side of the CI-posture alignment (docenta commit 63cf6f6), which added the same four properties to its copy of this generator. Four unit tests come with it.
codeql-action init and analyze 4.37.8 -> 4.37.9, release-plz/action 0.5.131 -> 0.5.132, softprops/action-gh-release to its current v3 SHA. Supersedes dependabot PR #622. The new cross-workflow consistency gate added earlier in this series confirms all five pins land on one SHA per action.
`cargo deny` failed the ship on rustls 0.23.43: it accepted TLS 1.3 handshake messages sent at the wrong encryption level when they followed a key-changing message in the same record, so a plaintext `EncryptedExtensions` packed into the same record as `ServerHello` was accepted. RFC 8446 section 5.1 requires the connection be terminated with an "unexpected_message" alert instead. The transcript stays authenticated, so this is not a handshake-forgery path; the practical effect is that a peer can send messages in plaintext that should have been encrypted without rustls objecting. Same bug as Go CVE-2025-61730. rustls reaches UFFS transitively through polars -> object_store -> reqwest -> hyper-rustls, so no direct dependency changes. The fix is in the deframer: `is_aligned` now means "no pending handshake message, complete or partial" rather than "no partial fragment". Audited in the same commit, along with the accompanying `PeerMisbehaved` tightening and the `NonEmpty` type parameter on `PayloadU24`.
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.
Summary
just shipPhase 2 auto-commit for v0.6.41 — the[workspace.package].versionbump inCargo.toml. This PR routes that commit through branch-protection rules. Once it merges tomain, runjust release-tagto cut the signedv0.6.41tag, which firesrelease.ymland builds the cross-platform binaries + GitHub Release v0.6.41. (No auto-tag on merge — the tag step is manual on-demand, Path B.)Auto-merge
--auto --squashis queued — GitHub will merge as soon as the required status checks pass. Squash is required becausemain-protectionmandates signed commits, and GitHub's rebase-auto-merge cannot sign the rebased commit; the squash-merge commit is signed by GitHub's own key, which satisfiesrequired_signatures: true. The original author's signed commit remains verifiable in the PR branch history.After merge
The auto-commit lived only on
release/v0.6.41, so localmainnever drifted — sync it with a plaingit pull --ff-only origin main(noreset --hardneeded).