Match macOS du allocation accounting - #379
Open
nshcr wants to merge 1 commit into
Open
Conversation
Mirror Apple FTS complete common/file attribute request and packed-invalid records so macOS allocation totals match /usr/bin/du. Reject unknown vnode types, round allocation bytes to 512-byte stat blocks, and fall back to symlink metadata for directories, firmlinks, and incomplete records. Layer optional APFS clone and data-fork attributes onto the same packed layout so the upstream clone-deduplication path remains intact.
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.
Note
AI development disclosure
Both the code changes in this PR and this description were produced by Codex under my direction. I defined the scope and guided the investigation; Codex performed the data collection, cross-version comparisons, source and history tracing, implementation, test development and execution, validation, and final drafting.
I manually reviewed the relevant primary sources and found no serious issues. If this approach is incorrect or does not fit
dua's direction, please feel free to modify or reject it, or let me know how it should be improved!Background
Hi! I found this issue unexpectedly while using
duaon macOS 26. When scanning an OrbStack directory mounted through NFS,dua's default allocated-size total did not match the equivalent result from/usr/bin/du.At first, this looked like a single NFS regression. Testing several
duareleases showed that the discrepancy had actually changed direction:Further checks ruled out hard-link deduplication, traversal concurrency, and cancellation between subtrees. The discrepancy came from the allocated-size metadata returned by three different macOS query contracts.
This PR assumes that matching the system
dubehavior may be desirable fordua, at least for default allocated-size accounting on macOS. It therefore follows the metadata contract used by Apple FTS - the traversal implementation behind macOS/usr/bin/du- rather than adding an OrbStack/NFS-specific workaround.Reproduction
The comparison uses equivalent default allocated-size modes:
duareports bytes directly. WithBLOCKSIZE=512, macOSdureports 512-byte blocks:The main comparison was run sequentially against the same mounted tree:
/usr/bin/du/usr/bin/duThese runs traversed 140,829 entries and reported the same 13 inaccessible paths. A later v2.39.1 check reproduced the v2.41.x byte total, although that older release propagated traversal errors differently. This also shows that the v2.40 walker was not the origin of the overcount.
OrbStack is a live filesystem, so absolute totals describe individual snapshots. The relevant evidence is the stable version-to-version difference under matching entry and error counts, followed by repeated exact agreement between the fix and
duon later snapshots.Root cause
Before v2.42.0,
duaultimately accounted allocated size using per-entry Unix metadata andst_blocks × 512. That is a conventional Unix definition, but it does not guarantee the same filesystem query contract as Apple FTS. On this NFS mount, the per-path metadata query returned a total 258,104,832 bytes larger than/usr/bin/du.PR #367 introduced a native macOS walker using
getattrlistbulk. Its original implementation:getattrlistbulk(..., options = 0);stat;These differences were not exposed by the existing local-filesystem tests, but on this NFS mount they changed the discrepancy from an overcount to an undercount of 13,035,008 bytes.
PR #369 then reused the same bulk metadata for aggregation roots. This was not the primary cause, but it extended the affected path and reduced the total by another 1,536 bytes on this snapshot.
Implementation
This PR makes
dua's macOS bulk walker follow the relevant Apple FTS contract:stat.statfields from bulk records.FSOPT_PACK_INVAL_ATTRSfor full metadata enumeration.st_blocks.The branch is based on #371. Its optional APFS data-fork allocation, clone ID, extended flags, and
FSOPT_ATTR_CMN_EXTENDEDbehavior are retained as extensions of the same packed layout. Missing APFS extension attributes disable clone metadata without invalidating an otherwise complete FTS-compatible record.The implementation does not inspect the filesystem name and contains no NFS-specific branch.
Validation
The following checks passed on the current branch:
All 126 automated tests passed. The macOS coverage includes bulk metadata, APFS clones and data forks, resource forks, symlinks, hard links, permissions, buffer refills, fallback behavior, FTS/APFS request shapes, vnode validation, returned-attribute completeness, and allocation rounding.
Real OrbStack NFS validation confirmed that:
/usr/bin/duexactly;ubuntuanddockersubtrees matched independently, ruling out cancellation between positive and negative errors;--count-hard-linksmatched/usr/bin/du -lexactly;--apparent-sizeremained unchanged from v2.42.1;After rebasing onto #371, the current head was checked again on a later filesystem snapshot:
The absolute totals changed with the live filesystem, but equality with the corresponding
dumode remained exact. The rebased head also produced the same result with one thread and the default eight threads.No automated real-NFS fixture is included. Unit tests protect the query, parsing, validation, fallback, and rounding rules; the OrbStack runs provide end-to-end evidence for the original issue.
Performance
A controlled benchmark performed before the rebase onto #371 compared v2.42.1 with the initial fix. Both binaries used Rust 1.95.0 and the same release/all-features configuration. After one warm-up per binary, each workload ran 12 times in alternating order:
The NFS workload was approximately 17 ms slower on average. The APFS difference was smaller than the observed run-to-run variation. These two workloads did not show a material regression, but they do not cover every filesystem, tree shape, or cache state.
This benchmark predates the rebase onto #371. The rebased head has passed correctness and APFS clone tests, but it has not been re-benchmarked against the current
main.Compatibility and limitations
--apparent-sizeor attempt byte-for-byte parity with macOSdu -A.References
getattrlistbulkcallst_blocksconstructiondu: FTS traversal andst_blocksaggregationgetattrlist(2)documentation