[vm] check captured value depth when packing closures - #412
Open
fEst1ck wants to merge 1 commit into
Open
Conversation
Add a ValueDepthChecker visitor and check each captured value against max_value_nest_depth in PackClosure/PackClosureGeneric, closing the move-capture path that bypassed the aptos-labs#364 copy-time checks. Captured values count from their own root (boundary 128/129, matching upstream); a transactional test pins the pack and copy boundaries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fEst1ck
marked this pull request as ready for review
August 13, 2026 11:00
fEst1ck
requested review from
0xIcarus,
Primata,
ganymedio and
musitdev
as code owners
August 13, 2026 11:00
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.
Description
Adds a pack-time depth check for closure captured values in the Move VM interpreter.
The #364 backport (upstream aptos-labs#16983) bounds value depth in copy, equals/compare,
read_ref, and (de)serialization. However, closures packed from moved captures never go through any of those checks: a loop likelet g = f; f = |x| apply(g, x);re-captures byMoveLoc, so the captured chain grows without ever being copied, andPackClosure/PackClosureGenericconstruct values of unbounded depth. Function types are the only types whose value depth is not bounded by their static type (Packfor structs is covered by the type-depth check), so this was the remaining gap.The fix adds a
ValueDepthCheckervisitor inmove-vm-types(exposed asValue::check_max_depth) and calls it on each captured value in both pack arms, gated onenable_depth_checks/max_value_nest_depth.Boundary semantics: each captured value is counted from its own root (the new closure wrapper does not add a level), so captured values up to depth 128 are allowed and the packed closure may transiently reach total depth 129. This matches both the copy-time boundary (anything legal to copy is legal to capture) and upstream behavior as pinned by the existing
function_value_depth.rse2e test (run2(128)succeeds,run2(129)aborts). A closure packed at the limit aborts on later copy/compare/store, which count from the closure root; this transient overshoot is inherent (struct-packing over a max-depth closure field creates it too) and is documented and pinned by a test.How Has This Been Tested?
move-vm/transactional-tests/tests/instructions/closure_pack_depth.movepins the boundary with move-only captures (isolating the pack-time check):chain(128)packs and runs;chain(129)aborts withVM_MAX_VALUE_DEPTH_REACHEDat thePackClosure;chain(128)followed by an explicitcopyaborts at theCopyLoc.function_value_depth::test_vm_value_too_deep_with_function_values(from [vm] backport VM value depth checks (#16983) #364) passes:run2(128)succeeds,run2(129)aborts.move-vm-typesvalue_depth_testsunit tests: all pass.Key Areas to Review
check_captured_value_depthsininterpreter.rs.ValueDepthCheckerlevel counting (values_impl.rs): struct/vector/closure children at +1; primitive-vector contents add no level and are checked O(1) viavisit_vec_*overrides, matchingcopy_valueand the serializer; references descend (unreachable for captures, which cannot contain references).popnof the captures and beforeLazyLoadedFunction::new_resolved, preserving the fix: Security nits for MoveVM #354 verify-before-pack order.Type of Change
Which Components or Systems Does This Change Impact?
Checklist