Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions crates/vm/src/builtins/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::{
};
use core::sync::atomic::{AtomicU32, Ordering::Relaxed};
use itertools::Itertools;
use rustpython_common::atomic::Radium;
#[cfg(feature = "jit")]
use rustpython_jit::CompiledCode;

Expand Down Expand Up @@ -783,6 +784,170 @@ impl Py<PyFunction> {
}
self.invoke_prepared_exact_args(taken, vm)
}

/// Fast path for unobserved Python-to-Python calls.
/// Allocates a LightFrame on the DataStack instead of a full Frame PyObject.
/// The frame is materialized lazily only if observed (traceback, sys._getframe).
///
/// Falls back to `invoke_exact_args_slots` for generators/coroutines or when
/// tracing is active.
pub(crate) fn invoke_light_slots(
&self,
args: &mut [Option<PyObjectRef>],
vm: &VirtualMachine,
) -> PyResult {
use crate::frame::{ExecutionResult, FrameLocals, FrameSource, LightFrame, LocalsPlus};

let code: &Py<PyCode> = &self.code;

// Generator/coroutine code and tracing must use the heavy path
// Fall back to the heavy path for generators/coroutines, tracing,
// and non-optimized/non-NEWLOCALS code (e.g. types.FunctionType
// with a non-standard code object).
if code.flags.intersects(
bytecode::CodeFlags::GENERATOR
| bytecode::CodeFlags::COROUTINE
| bytecode::CodeFlags::ASYNC_GENERATOR,
) || vm.use_tracing.get()
|| !code
.flags
.contains(bytecode::CodeFlags::NEWLOCALS | bytecode::CodeFlags::OPTIMIZED)
{
return self.invoke_exact_args_slots(args, vm);
}

let nlocalsplus = code.localspluskinds.len();
let max_stackdepth = code.max_stackdepth as usize;
let alloc_size = LightFrame::alloc_size(nlocalsplus, max_stackdepth);

let base = vm.datastack_push(alloc_size);
let light = base as *mut LightFrame;

unsafe {
// Initialize the LightFrame header with borrowed pointers (NO refcount bumps)
core::ptr::write(
light,
LightFrame {
code: code as *const _,
globals: &*self.globals as *const _,
builtins: &*self.builtins as *const _,
func_obj: self.as_object() as *const _,
lasti: Radium::new(0),
prev_line: 0,
previous_light: crate::vm::thread::get_current_light_frame(),
saved_current_frame: crate::vm::thread::get_current_frame(),
materialized: core::cell::UnsafeCell::new(core::ptr::null_mut()),
nlocalsplus: nlocalsplus as u32,
max_stackdepth: max_stackdepth as u32,
},
);

// Zero-init the localsplus area
let lp_ptr = (*light).localsplus_ptr();
let capacity = nlocalsplus + max_stackdepth;
core::ptr::write_bytes(lp_ptr, 0, capacity);

// Move args into localsplus
let slots =
core::slice::from_raw_parts_mut(lp_ptr as *mut Option<PyObjectRef>, nlocalsplus);
for (slot, arg_slot) in slots.iter_mut().zip(args.iter_mut()) {
*slot = arg_slot.take();
}

// Copy closure cells if present
if let Some(ref closure) = self.closure {
let nfrees = code.freevars.len();
if nfrees > 0 {
let freevar_start = nlocalsplus - nfrees;
for (i, cell) in closure.iter().enumerate() {
slots[freevar_start + i] = Some(cell.clone().into());
}
}
}

// Push light frame onto TLS chain
let prev_light = crate::vm::thread::set_current_light_frame(light);

// Recursion depth and C stack overflow check
let depth = vm.current_recursion_depth();
if depth >= vm.recursion_limit.get() || (depth & 63 == 0 && vm.check_c_stack_overflow())
{
// Clean up the light frame TLS before erroring
crate::vm::thread::set_current_light_frame(prev_light);
// Drop values we moved into localsplus
let slots =
core::slice::from_raw_parts_mut(lp_ptr as *mut Option<PyObjectRef>, capacity);
for slot in slots.iter_mut() {
*slot = None;
}
vm.datastack_pop(base);
return Err(vm.new_recursion_error("maximum recursion depth exceeded".to_string()));
}
vm.recursion_depth_increment();

// Cache the materialized-field pointer so the panic guard doesn't
// need to borrow `light` (which is also used mutably for prev_line).
let materialized_cell: *const core::cell::UnsafeCell<*mut Py<Frame>> =
&(*light).materialized;

// Panic guard: restore TLS and recursion depth, reclaim materialized
// frame, pop DataStack. Localsplus values are dropped by
// LocalsPlus::drop (run_light_frame takes ownership).
// TLS restored first so destructors can re-enter the VM safely.
scopeguard::defer! {
vm.recursion_depth_decrement();
crate::vm::thread::set_current_light_frame(prev_light);
let materialized_ptr = *(*materialized_cell).get();
if !materialized_ptr.is_null() {
let reclaim = FrameRef::from_raw(materialized_ptr as *const _);
drop(reclaim);
}
vm.datastack_pop(base);
}

// Build LocalsPlus view over the light frame data
let localsplus = LocalsPlus::from_datastack_raw(lp_ptr, capacity, nlocalsplus);

// Create lazy FrameLocals (NEWLOCALS fast path)
let locals = FrameLocals::lazy();

// Build the builtins_dict cache
let builtins_dict = if self.globals.class().is(vm.ctx.types.dict_type) {
self.builtins
.downcast_ref_if_exact::<super::PyDict>(vm)
.map(|d| crate::PyExact::ref_unchecked(d))
} else {
None
};

// Run the bytecode
let code_ref = (*(*light).code).to_owned();
Comment on lines +923 to +924

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai can this be confirmed?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- PyAtomic definitions and cfg gates ---'
rg -n -C 8 'PyAtomic|type .*Atomic|struct .*Atomic' crates --glob '*.rs' | head -n 280

printf '%s\n' '--- LightFrame declaration and lasti uses ---'
rg -n -C 12 '\bstruct\s+LightFrame\b|\blasti\b' crates/vm/src --glob '*.rs' | head -n 360

printf '%s\n' '--- workspace threading feature declarations ---'
rg -n -C 4 '^\s*threading\s*=|feature.*threading|\bthreading\b' Cargo.toml crates --glob 'Cargo.toml' | head -n 240

Length of output: 46644


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate atomic and frame files ---'
fd -t f -e rs 'atomic|frame' crates/common crates/vm | sort

printf '%s\n' '--- exact PyAtomic definition ---'
rg -n -C 24 'pub\s+(type|use)\s+PyAtomic\b|PyAtomic\s*[=<]' crates/common --glob '*.rs'

printf '%s\n' '--- exact LightFrame definition and runner signature ---'
rg -n -C 28 '\bstruct\s+LightFrame\b|fn\s+run_light_frame\b|impl\s+LightFrame\b' crates/vm/src/frame.rs

printf '%s\n' '--- package feature wiring relevant to wasm and common ---'
sed -n '1,80p' crates/wasm/Cargo.toml
sed -n '1,55p' crates/common/Cargo.toml
sed -n '1,45p' crates/vm/Cargo.toml

Length of output: 20133


@fanninpm, confirmed.

PyAtomic<u32> is feature-dependent in crates/common/src/atomic.rs:

  • with threading: AtomicU32
  • without threading: core::cell::Cell<u32>

But LightFrame::lasti is unconditionally declared as core::sync::atomic::AtomicU32 (in crates/vm/src/frame.rs, Line 72), then cast to &PyAtomic<u32> in the new call path. The wasm package explicitly builds the VM without the threading feature, so that cast becomes effectively:

&AtomicU32 -> &Cell<u32>

That is unsound. A matching size/alignment today would not make it valid: the types have no guaranteed layout equivalence, and Cell operations perform interior mutation through a reference that does not actually point to a Cell.

The appropriate fix is to make LightFrame::lasti a PyAtomic<u32>, initialize it with Radium::new(0), and pass &(*light).lasti directly—removing the cast entirely.

One wording refinement: the unsafe raw-pointer cast likely bypasses a normal compile-time type mismatch, so I can confirm the non-threading unsoundness rather than specifically a guaranteed build failure.

let result = crate::frame::run_light_frame(
&code_ref,
localsplus,
&locals,
&self.globals,
&self.builtins,
builtins_dict,
FrameSource::Light(light),
self.as_object(),
&(*light).lasti,
&mut (*light).prev_line,
vm,
);

// Normal exit: the scopeguard handles all cleanup.

// Convert result
match result {
Ok(ExecutionResult::Return(value)) => Ok(value),
Ok(ExecutionResult::Yield(_)) => {
unreachable!("light frame should never yield")
}
Err(e) => Err(e),
}
}
}
}

pub(crate) fn datastack_frame_size_bytes_for_code(code: &Py<PyCode>) -> Option<usize> {
Expand Down
Loading
Loading