Repository navigation
fix(storage): store FULL when a delta base alone exceeds the chain budget - #473
Merged
yifanxuaaa merged 1 commit intoOct 27, 2026
Merged
Conversation
…dget At caebd26, selecting a whole-file base that alone exceeds the 512 KiB canonical or 256 KiB encoded budget can abort Save with a dependency-work Integrity error. Acquisition runs before the selector's prospective-budget check, so the existing work_exceeded result cannot handle such a base. Supported construction cutoffs above the default can produce these files. Extend ChainCost with the encoded cost obtained from stored records. Refuse an over-budget base before dependency reconstruction/authentication and store the new object independently with work_exceeded. When the base's own chain fits, acquire and authenticate it before applying the unchanged prospective check. Both refusals share refused_by_budget. Keep EncodedRecord::charged_width so cached payload costs account for the eight length-field bytes removed by compact whole-file assembly. Pooled metadata retains its reader's separate encoded accounting. Update Storage architecture section 6.8 and the delta handbook. Authentication is preserved for fitting base chains, including when the dependent would push the prospective chain over budget. Over-budget chains are not reconstructed or canonically authenticated; malformed positive-depth chains can therefore lose errors the parent encountered before its cumulative budget refusal. Valid producer output does not contain over-budget PREFIX chains. Errors encountered during the stored cost walk still propagate. Outside-sandbox validation I ran on Colab Linux x86_64 with Rust 1.85.1, Disposable persistence and LAYERFS_CONSTRUCTION_WORKERS=1: - delta_base_budget: 12 passed; parent production with these tests: 7 passed, 5 failed. - sdk_large_file_budget: 5 passed; parent: 1 passed, 4 failed. - The new corruption guard passes on parent and fix, and fails with 1165ad746 production code because that version silently stores FULL. - Storage nextest: 90 passed. Persistence: 99 passed, 1 skipped. - Fast lane: 1090 passed, 1 failed, 11 skipped; one binary skipped. The remaining local_install filesystem refusal also occurs on parent. - Scoped Storage/API Clippy, formatting and archived-snapshot boundary checks with --skip-sdk pass; tools self-tests: 155 passed. Rust targets were built before execution and test runs had explicit timeouts of at most 120 seconds. SDK coverage uses Rust calls and a private Workspace fixture, without a FUSE mount. Save time, mounted FUSE Commit, macOS and Windows were not measured or exercised. Durable: NOT_RUN — disabled by owner until explicit reauthorization. Production LOC: 102,200 -> 102,253 (delta +53) core 38,241 -> 38,294; layerfs-storage 8,993 -> 9,046. engine 61,313 and api 2,646 unchanged. Reference absent (retired R9c; last counted 65,417 at tree 498dd19). Method: tools/production_loc.py, blob 3341e4d, --root <snapshot> --detail on archived caebd26 and the parent with the final patch applied; the patch matches the committed tree. Tests, documentation and tooling excluded. Signed-off-by: Jackmeson1 <130693642+Jackmeson1@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019TTGAv6fe859ebbCLLfLHP
4 of 5 tasks
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
At
caebd268d, Save can fail when its selected delta base alone exceeds a dependency-work budget: 512 KiB canonical or 256 KiB encoded. Acquisition returnsIntegrity("dependency canonical work")orIntegrity("dependency encoded work")before selection reaches the existing independent-storage decision.Stores created with supported cutoffs above the default can contain whole files large enough to trigger this. I observed failures through
fs.apply,fs.write, a private Workspace Commit fixture, and Project Init of a directory containing two similar files. The failure requires selection of such a base.Selection now checks the base chain's own stored costs before dependency reconstruction/authentication. An over-budget base produces independent storage with
work_exceeded. A base whose own chain fits is acquired and authenticated first, followed by the unchanged prospective-budget check.Reproduction and cause
The issue contains a runnable storage reproduction and separate SDK observations.
With
StoragePolicy::new(1, 1_048_576, 16, 8), save a 300,000-byte incompressible whole-file object, then a near-identical version declaring the first as its base. Oncaebd268d, the second Save returnsIntegrity("dependency encoded work"). A 600,000-byte compressible pair returnsIntegrity("dependency canonical work").On that parent,
encoding/delta/select.rs:433acquires the base before the prospective test at line 446.Resolver::resolve_dependencycharges the base itself and every dependency against the same budgets. A valid base that cannot fit alone therefore aborts Save before itswork_exceededbranch.With parent production code and this PR's tests, five of the twelve storage cases and four of the five SDK cases fail with these budget errors. The remaining cases are controls or guards.
Implementation
ChainBases::edgereturns the stored base identity and the encoded width charged byResolver::decode_at, using the sharedselected_record_widthcalculation.ChainCostgainsencoded. The boundedDepthCacheaccumulates it alongside canonical cost. Eligibility already walks this chain, so the selected base's subsequent cost lookup normally hits the cache.selectrefuses a base whose own chain exceeds either budget before acquisition. Otherwise it acquires and authenticates the base and applies the parent's prospective test unchanged: acquisition counters plus the dependent's canonical length against both budgets.refused_by_budget, preserving the counter, independent-record result, candidate insertion and depth-zero accounting.EncodedRecord::charged_widthrecords the payload width after assembly. Compact whole-file assembly drops eight bytes of length fields; chunk and singleton records keep their framed widths. This helper is not an ordinary-group accounting function.The budgets, candidate ordering, resolver checks and strict
>boundary remain unchanged. An exact fill is admitted. The prospective check still uses authenticated acquisition counters; the preliminary cost walk does not add a second charge to them.Canonical identities, stored formats and the JavaScript SDK method surface are unchanged. The Rust
ChainCostandDepthCachesource surface changes, and the in-tree callers are updated.Authentication boundary
A selected base whose measured own-chain costs fit both budgets is acquired and authenticated before prospective-budget refusal. Payload corruption that changes the reconstructed identity therefore still fails with
Integrity("dependency identity"), including when the dependent would push the chain over a budget. Other errors encountered during acquisition also propagate.An over-budget base is neither reconstructed nor canonically authenticated. Establishing its cost still reads and parses stored records, and failures encountered during that walk propagate.
This is not a guarantee of identical error precedence for every malformed over-budget chain. In a positive-depth chain, the parent could encounter identity or decoding corruption before reaching the cumulative budget crossing. The early refusal can skip that error, as well as resolver-only structural checks. The producer does not emit over-budget PREFIX chains under the same limits, but corruption can create malformed stored chains. The new object is stored independently and does not depend on the refused base.
Missing advisory candidates retain the existing eligibility behavior; this change does not make every missing candidate or descendant a Save error.
Tests
core/crates/layerfs-storage/tests/delta_base_budget.rsadds twelve cases.Five regressions fail on parent production code:
The oversized-base cases assert independent storage,
work_exceeded = 1, no delta trial and complete canonical readback.Controls and guards cover exact prospective fills and one-byte excesses, equivalent refusal boundaries within one Save and across separate Saves, ordinary chaining, disabled whole-file deltas, and two corruption cases.
The corruption tests flip a stored payload byte and recompute the pack digest. One pair fits both prospective budgets. In the other, the base fits alone but the dependent exceeds the prospective encoded limit:
Both corruption guards pass on parent production code and this change. With
1165ad746production code, the second guard fails because Save silently returns an independent FULL result.engine/crates/layerfs-api/tests/sdk_large_file_budget.rsadds five cases:fs.apply, second 600,000-byte text versionfs.write, second 300,000-byte incompressible versionCommittedresult and size; first version installed throughfs.applyfs.applypair; sizeThe fixture gains
built_withto select the Store policy and retains explicit Disposable persistence. These tests do not provide complete byte-for-byte SDK readback or JavaScript-package integration coverage.Validation
I ran these checks outside the sandbox on Google Colab Linux, x86_64 with 8 vCPUs, using Rust 1.85.1 and
LAYERFS_CONSTRUCTION_WORKERS=1. Persistent Stores used Disposable.Rust test targets were built with
--no-runbefore execution. Focused tests ran under 100-second timeouts, the storage reproduction under 30 seconds, and nextest selections under 120 seconds.caebd268dlayerfs-storage --test delta_base_budgetlayerfs-api --test sdk_large_file_budgeta_delta_budget_reproThe crate runs used:
The fast lane used:
The remaining failure on both arms is
layerfs-api::local_install import_installs_the_store_in_process. It reports that filesystem0x794c7630does not carry SQLite locks between kernels and that direct Store access requires ext4, xfs or btrfs. The fast lane is not green. The parent's other nine failures are the five storage and four SDK regressions.Additional checks:
cargo +1.85.1 clippy --locked -p layerfs-storage --all-targets -- -D warnings: clean.layerfs-api: clean.cargo +1.85.1 fmt --manifest-path Cargo.toml --all -- --check: clean.timeout 110s python3 -B tools/check_product_boundary.py --skip-sdk: PASS on archived parent and final snapshots.timeout 110s python3 -B -m unittest discover -s tools -p 'test_*.py': 155 passed.The earlier boundary-guard run in
/root/lfs/srcreports violations in unchangedcodec.rsandfile_control.rs. That failure is retained separately; clean archived snapshots pass.Two throwaway boundary targets, not included in this PR, compare parent production code and this change. After two 100,000-byte versions, the third version is refused from 162,059 bytes on both, within one Save or across separate Saves. Across 26 size pairs, outcomes agree except for five parent budget errors corrected here.
A separate throwaway mutation replaced
charged_width()withwidth()for newly selected PREFIX cache entries. All twelve storage tests still passed. I retaincharged_widthto keep cached payload costs equal to stored-record costs; the current tests do not distinguish that adjustment.I did not measure Save time. Mounted FUSE Commit, macOS and Windows were not run. Durable:
NOT_RUN — disabled by owner until explicit reauthorization.Source size and related work
Production LOC: 102,200 -> 102,253 (delta +53)
All growth is in
layerfs-storage: 8,993 -> 9,046. Core totals are 38,241 -> 38,294; engine remains 61,313 and API remains 2,646.Reference absent (retired R9c; last counted 65,417 at tree 498dd1917812).
Method:
tools/production_loc.py, blob3341e4d0362c822b6d9a08419c432942b3a13800, with--root <snapshot> --detail, on archivedcaebd268dand that parent with the final patch applied. The retained final patch matches the committed tree. Tests, documentation and tooling are excluded.The retained search found no matching open duplicate; it included open roadmap #446. Closed #228 fixed charging unrelated grouped bytes and retained these budgets. #415 also edits
05-storage.md, which is a possible textual conflict.Fixes #472
AI disclosure: this was prepared with AI assistance (Claude Opus 5.5).
🤖 Generated with Claude Code
https://claude.ai/code/session_019TTGAv6fe859ebbCLLfLHP