-
Notifications
You must be signed in to change notification settings - Fork 3.9k
[vm] Fix script hot-state promotion determinism #20182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -144,7 +144,10 @@ use move_vm_runtime::{ | |
| InstantiatedFunctionLoader, LegacyLoaderConfig, ModuleStorage, RuntimeEnvironment, | ||
| ScriptLoader, WithRuntimeEnvironment, | ||
| }; | ||
| use move_vm_types::gas::{DependencyKind, GasMeter, UnmeteredGasMeter}; | ||
| use move_vm_types::{ | ||
| gas::{DependencyKind, GasMeter, UnmeteredGasMeter}, | ||
| sha3_256, | ||
| }; | ||
| use num_cpus; | ||
| use once_cell::sync::OnceCell; | ||
| use rand::RngCore; | ||
|
|
@@ -969,6 +972,26 @@ impl AptosVM { | |
| } | ||
|
|
||
| dispatch_loader!(code_storage, loader, { | ||
| // Record the script's declared dependencies before any fallible step: a | ||
| // kept-but-failed script still feeds its reads into the consensus-visible promotion | ||
| // set, which must not depend on verified-script-cache warmth. Probing the script | ||
| // cache is fine (it is keyed by the byte hash), and if deserialization fails, | ||
| // `load_script` fails the same way below. | ||
| let hash = sha3_256(serialized_script.code()); | ||
| let compiled_script = match code_storage.get_script(&hash) { | ||
| Some(code) => Some(Arc::clone(code.deserialized())), | ||
| None => code_storage | ||
| .runtime_environment() | ||
| .deserialize_into_script(serialized_script.code()) | ||
| .ok() | ||
| .map(|script| code_storage.insert_deserialized_script(hash, script)), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium severity — this now lets kept-failing scripts accumulate in the shared per-block script cache. On a cache miss, this new block inserts the deserialized A block full of distinct scripts that deserialize but later fail verification or the later script-only checks will now leave one cached entry per hash until block end. |
||
| }; | ||
| if let Some(compiled_script) = compiled_script { | ||
| for (address, module_name) in compiled_script.immediate_dependencies_iter() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium severity — unverified dependencies can now enter This block records With the default |
||
| code_storage.record_module_read(address, module_name); | ||
| } | ||
| } | ||
|
|
||
| let legacy_loader_config = LegacyLoaderConfig { | ||
| charge_for_dependencies: self.gas_feature_version() >= RELEASE_V1_10, | ||
| charge_for_ty_tag_dependencies: self.gas_feature_version() >= RELEASE_V1_27, | ||
|
|
@@ -986,15 +1009,6 @@ impl AptosVM { | |
| self.reject_unstable_bytecode_for_script(script)?; | ||
| event_validation::verify_no_event_emission_in_compiled_script(script)?; | ||
|
|
||
| // Record the script's declared module dependencies as reads. These are a function of | ||
| // the script bytecode, so recording them here keeps the read set independent of the | ||
| // verified-script cache: its warmth depends on the execution schedule (parallel | ||
| // interleaving, aborts), so deriving these reads from cache-gated dependency fetches | ||
| // would make the hot-state promotion set nondeterministic across nodes. | ||
| for (address, module_name) in script.immediate_dependencies_iter() { | ||
| code_storage.record_module_read(address, module_name); | ||
| } | ||
|
|
||
| let args = dispatch_transaction_arg_validation!( | ||
| session, | ||
| &loader, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| [package] | ||
| name = "hot_state_emit_test" | ||
| version = "0.0.0" | ||
|
|
||
| [dependencies] | ||
| AptosFramework = { local = "../../../../../framework/aptos-framework" } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| script { | ||
| use aptos_framework::coin; | ||
| use aptos_framework::aptos_coin::AptosCoin; | ||
| use aptos_framework::event; | ||
| use 0xcafe::emitter; | ||
|
|
||
| /// Event emission gets this script rejected (kept-and-failed) only after `load_script` has | ||
| /// cached the verified script. The dependencies deliberately mix special-address framework | ||
| /// modules with a non-special user module (`emitter`). | ||
| fun main() { | ||
| let _ = coin::supply<AptosCoin>(); | ||
| event::emit(emitter::make()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| module 0xcafe::emitter { | ||
| // Minimal event type so the script can call `0x1::event::emit`. | ||
| #[event] | ||
| struct Marker has store, drop { value: u64 } | ||
|
|
||
| public fun make(): Marker { | ||
| Marker { value: 0 } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Medium severity — the determinism fix is still incomplete for package publishes.
resolve_pending_code_publish_and_finish_user_session()still charges old modules and immediate dependencies from merely deserializedCompiledModulemetadata before later checks reject the bundle. The pre-validation loops ataptos_vm.rs:1632-1684callunmetered_get_module_size/unmetered_get_existing_module_size, andReadRecordingCodeStoragerecords those module keys into the transaction read set even when the lookup misses.A malformed publish transaction that later fails the sender-address check in
third_party/move/move-vm/runtime/src/storage/publishing.rs:158-172(or any later verification-status publish check) is kept rather than discarded, so those attacker-chosen module keys still flow throughaptos-move/aptos-vm/src/block_executor/vm_wrapper.rs:76-88into the block'sto_make_hotaccumulator whenHOTNESS_IN_EPILOGUEis enabled. That leaves the same consensus-visible promotion-poisoning primitive alive for package publishes even though the script path is being hardened here.