Skip to content

Commit 1e46482

Browse files
committed
Refund transient VM linear-memory budget at VM teardown
The memory budget has always measured cumulative memory -- the sum of all allocations -- and does not credit back transient memory that is later freed. For most invocations cumulative memory stays close to peak live memory, so this is a fine proxy. But each VM instantiation charges the contract's WASM linear memory (~1 MiB with default toolchain settings), which is freed at VM teardown; under deep or wide cross-contract calling, many VMs are created and destroyed and cumulative memory diverges sharply from peak, significantly overcharging for memory never held at once. Track the mem_bytes charged for each VM store's linear memory in its VmStoreData (initial allocation + runtime grows), and refund it to the budget's memory dimension when the store -- and thus its linear memory -- is freed at Drop. Scope is VM linear memory only; CPU is unaffected. The refund credits only the real budget total, never the shadow budget. Adds Budget::refund_mem and BudgetDimension::refund, and updates the affected metering assertions.
1 parent 699a075 commit 1e46482

13 files changed

Lines changed: 410 additions & 87 deletions

File tree

soroban-env-host/src/budget.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,6 +1332,13 @@ impl Budget {
13321332
.get_memory_cost(ty, 1, input)
13331333
}
13341334

1335+
/// Refunds `amount` to the memory dimension (transient linear memory freed
1336+
/// at VM teardown). Saturating; never refunds more than was charged.
1337+
pub(crate) fn refund_mem(&self, amount: u64) -> Result<(), HostError> {
1338+
self.0.try_borrow_mut_or_err()?.mem_bytes.refund(amount);
1339+
Ok(())
1340+
}
1341+
13351342
/// Runs a user provided closure in shadow mode -- all metering is done
13361343
/// through the shadow budget.
13371344
///

soroban-env-host/src/budget/dimension.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,18 @@ impl BudgetDimension {
140140
self.shadow_total_count = 0;
141141
}
142142

143+
/// Credits `amount` back to this dimension (used to refund transient
144+
/// allocations, e.g. a VM's linear memory freed at teardown). Saturating,
145+
/// so it can never underflow or refund more than was charged.
146+
///
147+
/// Only the real total is refunded: VM linear memory is charged to the real
148+
/// budget (never the shadow budget), so decrementing `shadow_total_count`
149+
/// here would corrupt shadow accounting (e.g. extend the debug-logging
150+
/// budget).
151+
pub(crate) fn refund(&mut self, amount: u64) {
152+
self.total_count = self.total_count.saturating_sub(amount);
153+
}
154+
143155
pub(crate) fn check_budget_limit(&self, is_shadow: IsShadowMode) -> Result<(), HostError> {
144156
let over_limit = if is_shadow.0 {
145157
self.shadow_total_count > self.shadow_limit

soroban-env-host/src/budget/wasmi_helper.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ pub(crate) const WASMI_LIMITS_CONFIG: WasmiLimits = WasmiLimits {
2525
};
2626

2727
// The `ResourceLimiter` is implemented for each VM store's data (rather than
28-
// for `Host`, which is shared by all stores): wasmi invokes the limiter on the
29-
// growing store's own data.
28+
// for `Host`, which is shared by all stores) so that the linear-memory
29+
// charges accumulate structurally in the store that owns the growing memory:
30+
// wasmi invokes the limiter on the growing store's own data, both during
31+
// instantiation and for runtime `memory.grow`.
3032
impl ResourceLimiter for VmStoreData {
3133
fn memory_growing(
3234
&mut self,
@@ -60,11 +62,36 @@ impl ResourceLimiter for VmStoreData {
6062
.map_err(|_| errors::MemoryError::OutOfBoundsGrowth)?;
6163
}
6264

65+
// VM linear memory is charged only to the real budget and refunded
66+
// only from the real budget (see `Vm::execute_with_mem_refund`);
67+
// guest memory only grows during instantiation or execution,
68+
// neither of which runs in shadow mode. This is just a defensive
69+
// check.
70+
if self
71+
.host
72+
.as_budget()
73+
.is_in_shadow_mode()
74+
.map_err(|_| errors::MemoryError::OutOfBoundsGrowth)?
75+
{
76+
return Err(errors::MemoryError::OutOfBoundsGrowth);
77+
}
78+
6379
self.host
6480
.as_budget()
6581
.charge(ContractCostType::MemAlloc, Some(delta))
66-
.map(|_| true)
67-
.map_err(|_| errors::MemoryError::OutOfBoundsGrowth)
82+
.map_err(|_| errors::MemoryError::OutOfBoundsGrowth)?;
83+
84+
// Accumulate the charge in this store's data so that the VM's
85+
// usage scope can refund it when the VM is torn down (see
86+
// `Vm::execute_with_mem_refund`).
87+
let charged = self
88+
.host
89+
.as_budget()
90+
.get_memory_cost(ContractCostType::MemAlloc, Some(delta))
91+
.map_err(|_| errors::MemoryError::OutOfBoundsGrowth)?;
92+
self.linear_mem_charged = self.linear_mem_charged.saturating_add(charged);
93+
94+
Ok(true)
6895
} else {
6996
Err(errors::MemoryError::OutOfBoundsGrowth)
7097
}

soroban-env-host/src/host/frame.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -753,17 +753,19 @@ impl Host {
753753
}
754754
};
755755
let vm = self.instantiate_vm(id, wasm_hash)?;
756-
let relative_objects = Vec::new();
757-
self.with_frame(
758-
Frame::ContractVM {
759-
vm: Rc::clone(&vm),
760-
fn_name: *func,
761-
args: args_vec,
762-
instance,
763-
relative_objects,
764-
},
765-
|| vm.invoke_function_raw(self, func, args, treat_missing_function_as_noop),
766-
)
756+
vm.execute_with_mem_refund(self, |vm| {
757+
let relative_objects = Vec::new();
758+
self.with_frame(
759+
Frame::ContractVM {
760+
vm: Rc::clone(vm),
761+
fn_name: *func,
762+
args: args_vec,
763+
instance,
764+
relative_objects,
765+
},
766+
|| vm.invoke_function_raw(self, func, args, treat_missing_function_as_noop),
767+
)
768+
})
767769
}
768770

769771
fn instantiate_vm(&self, id: &ContractId, wasm_hash: &Hash) -> Result<Rc<Vm>, HostError> {
@@ -891,12 +893,12 @@ impl Host {
891893
match &instance.executable {
892894
ContractExecutable::Wasm(wasm_hash) => {
893895
let vm = self.instantiate_vm(contract_id, wasm_hash)?;
894-
Ok(vm.module.proto_version)
896+
vm.execute_with_mem_refund(self, |vm| Ok(vm.module.proto_version))
895897
}
896898
ContractExecutable::ExternalRef(external_ref) => {
897899
let wasm_hash = self.resolve_external_ref_wasm_hash(external_ref)?;
898900
let vm = self.instantiate_vm(contract_id, &wasm_hash)?;
899-
Ok(vm.module.proto_version)
901+
vm.execute_with_mem_refund(self, |vm| Ok(vm.module.proto_version))
900902
}
901903
ContractExecutable::StellarAsset => self.get_ledger_protocol_version(),
902904
}

soroban-env-host/src/host/invocation_metering.rs

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,7 +1092,7 @@ mod test {
10921092
expect![[r#"
10931093
InvocationResources {
10941094
instructions: 4146007,
1095-
mem_bytes: 2861644,
1095+
mem_bytes: 764460,
10961096
disk_read_entries: 0,
10971097
memory_read_entries: 2,
10981098
write_entries: 2,
@@ -1110,7 +1110,7 @@ mod test {
11101110
invocation: CreateContractEntryPoint,
11111111
resources: SubInvocationResources {
11121112
instructions: 4146007,
1113-
mem_bytes: 2861644,
1113+
mem_bytes: 764460,
11141114
disk_read_entries: 0,
11151115
memory_read_entries: 2,
11161116
write_entries: 2,
@@ -1146,7 +1146,7 @@ mod test {
11461146
expect![[r#"
11471147
InvocationResources {
11481148
instructions: 316541,
1149-
mem_bytes: 1135019,
1149+
mem_bytes: 86427,
11501150
disk_read_entries: 0,
11511151
memory_read_entries: 3,
11521152
write_entries: 0,
@@ -1173,7 +1173,7 @@ mod test {
11731173
),
11741174
resources: SubInvocationResources {
11751175
instructions: 316541,
1176-
mem_bytes: 1135019,
1176+
mem_bytes: 86427,
11771177
disk_read_entries: 0,
11781178
memory_read_entries: 3,
11791179
write_entries: 0,
@@ -1207,7 +1207,7 @@ mod test {
12071207
expect![[r#"
12081208
InvocationResources {
12091209
instructions: 320592,
1210-
mem_bytes: 1135562,
1210+
mem_bytes: 86970,
12111211
disk_read_entries: 0,
12121212
memory_read_entries: 3,
12131213
write_entries: 1,
@@ -1233,7 +1233,7 @@ mod test {
12331233
expect![[r#"
12341234
InvocationResources {
12351235
instructions: 315840,
1236-
mem_bytes: 1134867,
1236+
mem_bytes: 86275,
12371237
disk_read_entries: 0,
12381238
memory_read_entries: 3,
12391239
write_entries: 0,
@@ -1259,7 +1259,7 @@ mod test {
12591259
expect![[r#"
12601260
InvocationResources {
12611261
instructions: 322503,
1262-
mem_bytes: 1135918,
1262+
mem_bytes: 87326,
12631263
disk_read_entries: 0,
12641264
memory_read_entries: 3,
12651265
write_entries: 1,
@@ -1285,7 +1285,7 @@ mod test {
12851285
expect![[r#"
12861286
InvocationResources {
12871287
instructions: 316380,
1288-
mem_bytes: 1134935,
1288+
mem_bytes: 86343,
12891289
disk_read_entries: 0,
12901290
memory_read_entries: 3,
12911291
write_entries: 0,
@@ -1311,7 +1311,7 @@ mod test {
13111311
expect![[r#"
13121312
InvocationResources {
13131313
instructions: 317605,
1314-
mem_bytes: 1135287,
1314+
mem_bytes: 86695,
13151315
disk_read_entries: 0,
13161316
memory_read_entries: 3,
13171317
write_entries: 0,
@@ -1337,7 +1337,7 @@ mod test {
13371337
expect![[r#"
13381338
InvocationResources {
13391339
instructions: 318007,
1340-
mem_bytes: 1135287,
1340+
mem_bytes: 86695,
13411341
disk_read_entries: 0,
13421342
memory_read_entries: 3,
13431343
write_entries: 0,
@@ -1363,7 +1363,7 @@ mod test {
13631363
expect![[r#"
13641364
InvocationResources {
13651365
instructions: 317444,
1366-
mem_bytes: 1135355,
1366+
mem_bytes: 86763,
13671367
disk_read_entries: 0,
13681368
memory_read_entries: 3,
13691369
write_entries: 0,
@@ -1396,7 +1396,7 @@ mod test {
13961396
expect![[r#"
13971397
InvocationResources {
13981398
instructions: 320615,
1399-
mem_bytes: 1135822,
1399+
mem_bytes: 87230,
14001400
disk_read_entries: 2,
14011401
memory_read_entries: 1,
14021402
write_entries: 2,
@@ -1428,7 +1428,7 @@ mod test {
14281428
expect![[r#"
14291429
InvocationResources {
14301430
instructions: 323152,
1431-
mem_bytes: 1136269,
1431+
mem_bytes: 87677,
14321432
disk_read_entries: 3,
14331433
memory_read_entries: 0,
14341434
write_entries: 3,
@@ -1874,7 +1874,7 @@ mod test {
18741874
),
18751875
data: String(
18761876
ScString(
1877-
StringM(invocation resource limits are exceeded: instructions: 1788420 > 10, memory bytes: 1163561 > 20, contract events size bytes: 800 > 50, contract data key 'ContractData(LedgerKeyContractData { contract: Contract(ContractId(Hash(2e0ff7a55065f3a896723a964c3d9862a4722bfc77229fe4875f390ef2a0027e))), key: LedgerKeyContractInstance, durability: Persistent })' size: 48 > 25, contract data entry with key 'ContractData(LedgerKeyContractData { contract: Contract(ContractId(Hash(2e0ff7a55065f3a896723a964c3d9862a4722bfc77229fe4875f390ef2a0027e))), key: LedgerKeyContractInstance, durability: Persistent })' size: 104 > 35, contract code entry with key 'ContractCode(LedgerKeyContractCode { hash: Hash(1a2ee5a89dd2162a20e716b3e2de70a2d662480a9565350378661871bcf8e398) })' size: 1840 > 45),
1877+
StringM(invocation resource limits are exceeded: instructions: 1788420 > 10, memory bytes: 114969 > 20, contract events size bytes: 800 > 50, contract data key 'ContractData(LedgerKeyContractData { contract: Contract(ContractId(Hash(2e0ff7a55065f3a896723a964c3d9862a4722bfc77229fe4875f390ef2a0027e))), key: LedgerKeyContractInstance, durability: Persistent })' size: 48 > 25, contract data entry with key 'ContractData(LedgerKeyContractData { contract: Contract(ContractId(Hash(2e0ff7a55065f3a896723a964c3d9862a4722bfc77229fe4875f390ef2a0027e))), key: LedgerKeyContractInstance, durability: Persistent })' size: 104 > 35, contract code entry with key 'ContractCode(LedgerKeyContractCode { hash: Hash(1a2ee5a89dd2162a20e716b3e2de70a2d662480a9565350378661871bcf8e398) })' size: 1840 > 45),
18781878
),
18791879
),
18801880
},
@@ -1927,7 +1927,7 @@ mod test {
19271927
),
19281928
data: String(
19291929
ScString(
1930-
StringM(invocation resource limits are exceeded: instructions: 320868 > 10, memory bytes: 1135974 > 20, total footprint ledger entries: 5 > 4, disk read ledger entries: 2 > 1, disk read bytes: 3132 > 30, write ledger entries: 2 > 0, write bytes: 3132 > 40, contract data key 'ContractData(LedgerKeyContractData { contract: Contract(ContractId(Hash(ba863dea340f907c97f640ecbe669125e9f8f3b63ed1f4ed0f30073b869e5441))), key: Symbol(ScSymbol(StringM(key_1))), durability: Persistent })' size: 60 > 25, contract data key 'ContractData(LedgerKeyContractData { contract: Contract(ContractId(Hash(ba863dea340f907c97f640ecbe669125e9f8f3b63ed1f4ed0f30073b869e5441))), key: LedgerKeyContractInstance, durability: Persistent })' size: 48 > 25, contract data entry with key 'ContractData(LedgerKeyContractData { contract: Contract(ContractId(Hash(ba863dea340f907c97f640ecbe669125e9f8f3b63ed1f4ed0f30073b869e5441))), key: LedgerKeyContractInstance, durability: Persistent })' size: 104 > 35, contract code entry with key 'ContractCode(LedgerKeyContractCode { hash: Hash(fc644715caaead746e6145f4331ff75c427c965c20d2995a9942b01247515962) })' size: 3028 > 45),
1930+
StringM(invocation resource limits are exceeded: instructions: 320868 > 10, memory bytes: 87382 > 20, total footprint ledger entries: 5 > 4, disk read ledger entries: 2 > 1, disk read bytes: 3132 > 30, write ledger entries: 2 > 0, write bytes: 3132 > 40, contract data key 'ContractData(LedgerKeyContractData { contract: Contract(ContractId(Hash(ba863dea340f907c97f640ecbe669125e9f8f3b63ed1f4ed0f30073b869e5441))), key: Symbol(ScSymbol(StringM(key_1))), durability: Persistent })' size: 60 > 25, contract data key 'ContractData(LedgerKeyContractData { contract: Contract(ContractId(Hash(ba863dea340f907c97f640ecbe669125e9f8f3b63ed1f4ed0f30073b869e5441))), key: LedgerKeyContractInstance, durability: Persistent })' size: 48 > 25, contract data entry with key 'ContractData(LedgerKeyContractData { contract: Contract(ContractId(Hash(ba863dea340f907c97f640ecbe669125e9f8f3b63ed1f4ed0f30073b869e5441))), key: LedgerKeyContractInstance, durability: Persistent })' size: 104 > 35, contract code entry with key 'ContractCode(LedgerKeyContractCode { hash: Hash(fc644715caaead746e6145f4331ff75c427c965c20d2995a9942b01247515962) })' size: 3028 > 45),
19311931
),
19321932
),
19331933
},

soroban-env-host/src/host/lifecycle.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -313,22 +313,26 @@ impl Host {
313313
// native test contracts behave like wasm. They will never be
314314
// instantiated, this is just to exercise their storage logic.
315315
} else {
316-
let _check_vm = Vm::new(
316+
let check_vm = Vm::new(
317317
self,
318318
ContractId(Hash(hash_bytes.metered_clone(self)?)),
319319
wasm_bytes_m.as_slice(),
320320
)?;
321-
// At this point we do a secondary parse on what we've checked to be a valid
322-
// module in order to extract a refined cost model, which we'll store in the
323-
// code entry's ext field, for future parsing and instantiations.
324-
_check_vm.module.cost_inputs.charge_for_parsing(self)?;
325-
ext = crate::xdr::ContractCodeEntryExt::V1(crate::xdr::ContractCodeEntryV1 {
326-
ext: ExtensionPoint::V0,
327-
cost_inputs: crate::vm::ParsedModule::extract_refined_contract_cost_inputs(
328-
self,
329-
wasm_bytes_m.as_slice(),
330-
)?,
331-
});
321+
ext = check_vm.execute_with_mem_refund(self, |check_vm| {
322+
// At this point we do a secondary parse on what we've checked to be a valid
323+
// module in order to extract a refined cost model, which we'll store in the
324+
// code entry's ext field, for future parsing and instantiations.
325+
check_vm.module.cost_inputs.charge_for_parsing(self)?;
326+
Ok(crate::xdr::ContractCodeEntryExt::V1(
327+
crate::xdr::ContractCodeEntryV1 {
328+
ext: ExtensionPoint::V0,
329+
cost_inputs: crate::vm::ParsedModule::extract_refined_contract_cost_inputs(
330+
self,
331+
wasm_bytes_m.as_slice(),
332+
)?,
333+
},
334+
))
335+
})?;
332336
}
333337

334338
let hash_obj = self.add_host_object(self.scbytes_from_slice(hash_bytes.as_slice())?)?;

0 commit comments

Comments
 (0)