fix: account for canister history memory usage - #10825
Merged
Merged
Conversation
mraszyk
force-pushed
the
mraszyk/fail-on-canister-history-memory-usage
branch
2 times, most recently
from
July 20, 2026 09:17
847e77e to
1c22af4
Compare
…history `CanisterState::add_canister_change` previously recorded a canister history entry and returned the resulting execution-memory delta, which callers applied to the subnet available memory via `update_execution_memory_unchecked` (letting it go negative). This meant recording canister history never respected the subnet available execution memory. Introduce a checked `CanisterState::add_canister_change` that takes the subnet available memory, applies the delta, and fails with `InsufficientMemory` when the subnet cannot account for the additional canister history. The previous delta-returning behavior is preserved as `add_canister_change_unchecked`, used by `install_code` where the delta is accumulated and applied (checked) at commit time. All management endpoints except `rename_canister` are switched to the checked API: they operate on a clone of the `CanisterState` (and, via `execute_mgmt_operation_on_canister` or an internal round-limits snapshot, on restorable round limits), so failing after the change is recorded discards it. `rename_canister` is handled separately. `uninstall_code` gets a special case for the NNS governance canister, which can forcefully uninstall any canister: if the subnet cannot account for the `CanisterCodeUninstall` history entry, the canister history is dropped instead (as when a canister runs out of cycles) and the freed memory is returned to the subnet available execution memory, so the uninstall still succeeds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mraszyk
force-pushed
the
mraszyk/fail-on-canister-history-memory-usage
branch
from
July 20, 2026 09:32
1c22af4 to
de01fb6
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
This PR tightens execution-memory accounting by making canister-history recording respect subnet available execution memory, turning what was previously an “unchecked, can go negative” accounting path into a checked/fallible one for most management operations.
Changes:
- Split
CanisterState::add_canister_changeinto a new checked API (chargesSubnetAvailableMemoryand can fail) plus anadd_canister_change_uncheckedvariant that returns a delta for deferred/commit-time charging. - Switch most canister management operations to use the checked API and map
SubnetAvailableMemoryErrortoCanisterManagerError, with a special-case for NNS governance uninstall to drop history if memory is exhausted. - Update and add tests to account for canister-history charging against subnet memory and to validate uninstall behavior under memory exhaustion.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| rs/replicated_state/src/canister_state.rs | Introduces checked add_canister_change API and preserves old delta-returning behavior as add_canister_change_unchecked. |
| rs/execution_environment/tests/memory_matrix.rs | Separates “allocated bytes” from “subnet-charged allocated bytes” to include canister history in subnet-memory capacity assertions. |
| rs/execution_environment/src/scheduler/tests/charging.rs | Adds regression test ensuring out-of-cycles uninstall succeeds even with zero subnet available execution memory (history dropped). |
| rs/execution_environment/src/execution/install_code.rs | Switches install-code path to use unchecked canister-history recording with deferred accounting. |
| rs/execution_environment/src/canister_manager/tests.rs | Adds tests for uninstall failure when subnet can’t account for history, and governance override behavior that clears history. |
| rs/execution_environment/src/canister_manager.rs | Switches multiple operations to checked history recording; adds governance uninstall fallback and error mapping helper. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Address review feedback on the tests for failing when the subnet cannot account for canister history: - Fix `freed` in the uninstall tests: it now measures the memory that `uninstall_canister` actually credits back (execution state + Wasm chunk store) instead of `memory_allocated_bytes - history`, which was dominated by log memory that `clear_log` keeps allocated. This makes the "one byte too low" setup accurate and lets us assert `uninstall_change_bytes > freed`. - Assert `total_num_changes` behavior: unchanged on the atomic failure, bumped by one when governance drops the history and when the uninstall succeeds, and unchanged across the out-of-cycles uninstall (no `CanisterCodeUninstall` change recorded). - Move `uninstall_code_by_governance_clears_history_when_subnet_memory_exhausted` next to `uninstall_code_fails_if_subnet_cannot_account_for_canister_history` and align its structure. - Add `create_canister_fails_if_subnet_cannot_account_for_canister_history`: round limits are restored and no canister is inserted on failure. - Assert the succeeding uninstall drops canister memory usage by `freed - uninstall_change_bytes`. - Drop the manually recorded canister-history change in the out-of-cycles test and clarify comments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ry_usage_checks_and_updates Unify canister history accounting so that recording a canister change is always routed through `cycles_and_memory_usage_checks_and_updates`: - `CanisterState::add_canister_change` now simply records the change and returns the resulting `SubnetAvailableExecutionMemoryChange`; the checked variant (taking the subnet available memory) and the separate `add_canister_change_unchecked` are removed. - `cycles_and_memory_usage_checks_and_updates` takes an `Option<SubnetAvailableExecutionMemoryChange>` and folds it into the new execution memory used, so the recorded canister history is accounted for in the subnet available execution memory, the freezing threshold, and the storage reservation. - All management endpoints that record canister history (`update_settings`, `uninstall_code`, `create_canister`, `take_canister_snapshot`, `load_canister_snapshot`) record the change before calling the checks function and pass the delta to it; `uninstall_code` no longer updates the subnet available memory itself. - The NNS governance special case in `uninstall_code` is dropped: governance-initiated uninstall now fails uniformly if the subnet cannot account for the additional canister history entry. Because recording canister history now requires the canister to be solvent (and reserves storage cycles), tests creating/updating under-funded or frozen canisters are updated accordingly, including the shared `StateMachine::create_canister` helper (which defaults the freezing threshold to zero) and the `memory_matrix` suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…unting - Normalize the "accounted for via the `canister_history_change` argument" comment across all call sites and reword the folding/freezing comments in `cycles_and_memory_usage_checks_and_updates`. - Drop the now-unused `round_limits` parameter from `uninstall_canister`. - Reword the zero-freezing-threshold test comments and rename `history_before` to `history_memory_usage_before`. - Rewrite the create-canister subnet-memory-accounting tests to use `ExecutionTest` (with a zero log memory limit) as a succeeds/one-byte-below- fails pair, matching the uninstall tests. - Keep the subnet memory threshold at 0 in `create_canister_can_set_reserved_cycles_limit`, raising the reserved cycles limit so the `canister_creation` reservation fits. - In the memory matrix test, account for canister history uniformly (actual memory usage including history) and keep a single "without history" quantity for scenario classification; fix the stale comments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In the memory matrix test, only use the `_without_history` quantities in assertions (scenario classification), never in control flow. Gate the freezing-threshold invariant and the reserved-cycles/freezing sub-tests on the actual (with-history) allocated bytes instead, and drop the now-unused `RunResult::allocated_bytes_without_history` field. This re-enables the `FreezingThreshold::Short` invariant for operations that allocate only a small canister-history entry (e.g. `take_snapshot_and_uninstall`), where the cycles reserved for those few bytes are dominated by the freezing limit based on the pre-existing memory usage. Extend `skip_short_invariant` declaratively to cover `OtherManagement` + `MemoryUsageChange::None`, matching the existing `IncreaseLogAndMemoryAllocation` + `Large` exception. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reword the comments at the `settings_change`, `controllers_change`, and `canister_creation` call sites of `cycles_and_memory_usage_checks_and_updates` to spell out that `validate_and_update_canister_settings` has already applied the settings and run the cycles-and-memory-usage checks for the settings-driven memory change. Recording the canister history entry is therefore the only memory change past that point, justifying why the old and new memory usage passed to the call are equal (the entry itself being accounted for via the `canister_history_change` argument). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…emory once `take_canister_snapshot` previously called `cycles_and_memory_usage_checks_and_updates` twice: once (before the uninstall) for the snapshot allocation and instruction cost, and once (after the uninstall) for the `CanisterCodeUninstall` canister history entry. Collapse this into a single call at the end of the operation: - Take the snapshot and (optionally) uninstall the code first, then read `new_memory_usage` from the actual canister state (reflecting the snapshot and the uninstall) before recording the history entry, folding the entry in via the `canister_history_change` argument. This drops the hand-computed `uninstalled_canister_size`/`replace_snapshot_size` memory estimates in favor of the real post-mutation memory usage. Any partial state mutations are rolled back by the caller (`execute_mgmt_operation_on_canister`) if the check fails. As a result, the freezing threshold guarding the snapshot instruction charge now consistently reflects the canister's true final memory usage (including the uninstall history entry) instead of a pre-history value that only differed due to call ordering. At the exact affordability boundary this shifts which resource check trips first, so `take_snapshot_and_uninstall` can now fail with `CanisterOutOfCycles` instead of `InsufficientCyclesInMemoryGrow`. Update `test_minimum_cycles_balance` to accept either error for `OtherManagement`, matching the `Scenario::OtherManagement` arm already present in the sibling `test_freezing_threshold`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… memory usage Previously `cycles_and_memory_usage_checks_and_updates` received the memory usage *excluding* the canister history recorded by the operation, plus the history's allocated-bytes delta, and folded the latter into the former before applying `allocated_bytes()`. When the recorded history entry crossed the canister's memory allocation boundary, this double-counted the allocation and under-reserved storage cycles (reserving for roughly half the truly allocated bytes). Fix the fold by passing `new_memory_usage` *including* any recorded canister history: every caller now reads it after `add_canister_change`, so the subnet available memory, freezing threshold, and storage reservation are all computed once over the true total memory usage. The `canister_history_change` parameter and the fold are removed; `add_canister_change`'s return value is still consumed by the direct subnet-memory paths (`rename_canister`, `install_code`). In the memory matrix test, make the `Scenario` `memory_usage_change` reflect the overall change including canister history: drop the `_without_history` quantities, assert against the with-history memory usage/allocated bytes, reclassify `take_snapshot_and_uninstall` as `Increase` (its only net change is the `CanisterCodeUninstall` history entry), lower the `CrossedDuringTest` offset below one canister history entry so such tiny changes still cross, and skip the short-threshold freezing invariant whenever the operation's only new allocation is a canister history entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…story
Add a memory matrix scenario that uninstalls a canister with a minimal execution
state (an empty Wasm module, no chunks), so the memory freed by the uninstall is
smaller than the `CanisterCodeUninstall` canister history entry it records. The
canister's memory usage therefore increases overall, exercising the subnet-memory
and storage-reservation accounting for canister history recorded by an uninstall
(the ad-hoc `uninstall_code_{fails,succeeds}_if_..._canister_history` unit tests
covered this only for a single configuration).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…red by matrix The `uninstall_code_fails_if_subnet_cannot_account_for_canister_history` and `uninstall_code_succeeds_if_freed_execution_state_covers_canister_history` unit tests are now covered by the memory matrix suite: - the "fails" case by `test_memory_suite_uninstall_code_below_canister_history` (minimal execution state, freed < history entry) via `test_subnet_memory_capacity`, which asserts the uninstall fails with `SubnetOversubscribed` one byte below capacity; - the "succeeds" case by `test_memory_suite_uninstall_code` (`setup_universal_canister_with_much_memory`, freed >> history entry), whose net deallocation lets `test_subnet_memory_capacity` run the uninstall with the subnet at capacity and assert it succeeds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The returned execution-memory change is no longer passed to `cycles_and_memory_usage_checks_and_updates` (that parameter was removed): management endpoints now account for recorded canister history by including it in the memory usage they pass to that function and discard the returned change, while `rename_canister` and `install_code` still consume it directly. Update the doc comment accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…canister Route `rename_canister` through `cycles_and_memory_usage_checks_and_updates` (like canister creation) so that recording its `rename_canister` history entry is accounted for in the subnet available execution memory and the canister's cycles, failing with `SubnetMemoryCapacityOverSubscribed` when the subnet cannot account for it. The mutations (rename + history entry) and any partial `round_limits` update are rolled back on failure, keeping the rename atomic; the caller then puts the canister back under its unchanged old id. Add unit tests analogous to the canister-creation history tests covering the exactly-enough (succeeds) and one-byte-short (fails atomically) cases. With rename no longer consuming the change returned by `add_canister_change`, its return value is used by no caller (`install_code` now computes the allocated/deallocated delta itself via `memory_allocated_bytes()`), so drop it along with the now-unused `SubnetAvailableExecutionMemoryChange` type and the `update_execution_memory_unchecked` helper, and drop the `let _ =` at the remaining call sites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… canister history The end-of-round subnet memory usage invariant used to allow the total canister memory allocated bytes to exceed the subnet memory capacity by the total canister history memory usage, because canister history was not tracked in `SubnetAvailableMemory` during a round. Canister history is now accounted for in `SubnetAvailableMemory` like any other canister memory, so drop that slack and require the total canister memory allocated bytes (canister history included) to not exceed the subnet memory capacity. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ment The parenthetical in the `cycles_and_memory_usage_checks_and_updates` doc comment explained the design only by contrast with the old `allocated_bytes()`-folding approach this PR removed, so it justified the change rather than describing the current code. Drop it and keep the substantive sentence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nTest Drive the rename_canister subnet message end-to-end from the migration canister instead of calling canister_manager.rename_canister directly, so the tests also cover the subnet-message dispatch and response path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `requested_by` field of `RenameCanisterArgs` records the principal that requested the migration, distinct from the sender (always the migration canister). Use `test.user_id()` to reflect realistic usage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mraszyk
commented
Jul 24, 2026
The helper also sets a zero freezing threshold, not just a zero log memory limit. Rename it accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
✅ No security or compliance issues detected. Reviewed everything up to b6b4718. Security Overview
Detected Code Changes
|
schneiderstefan
approved these changes
Jul 29, 2026
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.
Recording a canister history entry previously bypassed the subnet's memory and cycles accounting:
add_canister_changeapplied the resulting execution-memory delta to the subnet available memory (letting it go negative), and it neither reserved storage cycles nor counted towards the freezing threshold. This PR makes canister history memory be accounted for like any other canister memory.Additionally,