Skip to content

Commit 12fd3dd

Browse files
fix(vm): bound native fill loops by op budget.
2 parents 9f0e977 + 5d1e2c5 commit 12fd3dd

3 files changed

Lines changed: 19 additions & 2 deletions

File tree

compiler/src/modules/vm/handlers/builtin_methods/numeric.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub fn to_bytes(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> {
2424
let length = match pos.first() { Some(v) if v.is_int() => v.as_int().max(0) as usize, None => 1, _ => return Err(cold_type("length must be an integer")) };
2525
// Length is user-controlled; cap it against the heap budget so a huge count errors instead of aborting in the allocator.
2626
if length > vm.heap.limit() { return Err(cold_heap()); }
27+
vm.charge_steps(length)?; // Charge the O(length) fill so a huge `length` hits the op budget, not a native spin.
2728
let big = byteorder_is_big(vm, pos.get(1))?;
2829
let mut v = n.unsigned_abs();
2930
let mut out = alloc::vec![0u8; length];

compiler/src/modules/vm/handlers/builtin_methods/string.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ fn justify(vm: &mut VM, recv: Val, pos: &[Val], right: bool) -> Result<(), VmErr
309309
match (cs.next(), cs.next()) { (Some(c), None) => c, _ => return Err(cold_type("The fill character must be exactly one character long")) }
310310
} else { ' ' };
311311
let pad = width.saturating_sub(s.chars().count());
312+
vm.charge_steps(pad)?; // Charge the actual O(pad) fill so a huge `width` hits the op budget, not a native spin.
312313
let fills: String = iter::repeat_n(fill, pad).collect();
313314
let out = if right { fills + &s } else { s + &fills };
314315
vm.alloc_and_push_str(out)
@@ -323,7 +324,14 @@ pub fn expandtabs(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> {
323324
let mut col = 0usize;
324325
for c in s.chars() {
325326
match c {
326-
'\t' => { let n = if ts == 0 { 0 } else { ts - (col % ts) }; for _ in 0..n { out.push(' '); } col += n; }
327+
// Guard each tab's expansion against the heap limit and op budget so a huge `ts` can't spin natively.
328+
'\t' => {
329+
let n = if ts == 0 { 0 } else { ts - (col % ts) };
330+
if out.len().saturating_add(n) > vm.heap.limit() { return Err(cold_heap()); }
331+
vm.charge_steps(n)?;
332+
for _ in 0..n { out.push(' '); }
333+
col += n;
334+
}
327335
'\n' | '\r' => { out.push(c); col = 0; }
328336
_ => { out.push(c); col += 1; }
329337
}
@@ -434,6 +442,8 @@ pub fn center(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> {
434442
let width = pos[0].as_int().max(0) as usize;
435443
// User-controlled width drives the output size; cap it so a huge value errors instead of aborting in the allocator.
436444
if width > vm.heap.limit() { return Err(cold_heap()); }
445+
// Charge the O(width) fill so a huge `width` hits the op budget, not a native spin.
446+
vm.charge_steps(width)?;
437447
let fill = if pos.len() > 1 {
438448
let f = val_to_str(vm, pos[1])?;
439449
let mut cs = f.chars();
@@ -456,6 +466,7 @@ pub fn zfill(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> {
456466
let width = pos[0].as_int().max(0) as usize;
457467
// User-controlled width drives the output size; cap it so a huge value errors instead of aborting in the allocator.
458468
if width > vm.heap.limit() { return Err(cold_heap()); }
469+
vm.charge_steps(width)?; // Charge the O(width) fill so a huge `width` hits the op budget, not a native spin.
459470
let nchars = s.chars().count();
460471
let out = if nchars >= width {
461472
s

compiler/tests/cases/vm.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2140,5 +2140,10 @@
21402140
{"src": "y = 0\nprint([x for x in [1, 2, 3] if (y := x) > 1])\nprint(y)", "output": ["[2, 3]", "3"]},
21412141
{"src": "print([x for row in [[1, 2], [3, 4]] for x in row if x > 1])", "output": ["[2, 3, 4]"]},
21422142
{"src": "s = \"\"\"line1\nline2\"\"\"\nprint(s)", "output": ["line1", "line2"]},
2143-
{"src": "print(...)", "output": ["Ellipsis"]}
2143+
{"src": "print(...)", "output": ["Ellipsis"]},
2144+
{"src": "(255).to_bytes(99999)\n(255).to_bytes(99999)\n(255).to_bytes(99999)", "output": [], "error": "operation limit"},
2145+
{"src": "'x'.ljust(999999999)", "output": [], "error": "MemoryError"},
2146+
{"src": "'1'.zfill(999999999)", "output": [], "error": "MemoryError"},
2147+
{"src": "'x'.center(999999999)", "output": [], "error": "MemoryError"},
2148+
{"src": "'\\t'.expandtabs(999999999)", "output": [], "error": "MemoryError"}
21442149
]

0 commit comments

Comments
 (0)