Skip to content

Commit f8a78e6

Browse files
authored
stack soft limit (RustPython#6754)
1 parent 941a7bb commit f8a78e6

File tree

4 files changed

+150
-0
lines changed

4 files changed

+150
-0
lines changed

Cargo.lock

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ memchr = { workspace = true }
7777
caseless = "0.2.2"
7878
flamer = { version = "0.5", optional = true }
7979
half = "2"
80+
psm = "0.1"
8081
optional = { workspace = true }
8182
result-like = "0.5.0"
8283
timsort = "0.1.2"

crates/vm/src/vm/mod.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ pub struct VirtualMachine {
8686
pub state: PyRc<PyGlobalState>,
8787
pub initialized: bool,
8888
recursion_depth: Cell<usize>,
89+
/// C stack soft limit for detecting stack overflow (like c_stack_soft_limit)
90+
c_stack_soft_limit: Cell<usize>,
8991
/// Async generator firstiter hook (per-thread, set via sys.set_asyncgen_hooks)
9092
pub async_gen_firstiter: RefCell<Option<PyObjectRef>>,
9193
/// Async generator finalizer hook (per-thread, set via sys.set_asyncgen_hooks)
@@ -228,6 +230,7 @@ impl VirtualMachine {
228230
}),
229231
initialized: false,
230232
recursion_depth: Cell::new(0),
233+
c_stack_soft_limit: Cell::new(Self::calculate_c_stack_soft_limit()),
231234
async_gen_firstiter: RefCell::new(None),
232235
async_gen_finalizer: RefCell::new(None),
233236
};
@@ -689,11 +692,127 @@ impl VirtualMachine {
689692
self.recursion_depth.get()
690693
}
691694

695+
/// Stack margin bytes (like _PyOS_STACK_MARGIN_BYTES).
696+
/// 2048 * sizeof(void*) = 16KB for 64-bit.
697+
const STACK_MARGIN_BYTES: usize = 2048 * std::mem::size_of::<usize>();
698+
699+
/// Get the stack boundaries using platform-specific APIs.
700+
/// Returns (base, top) where base is the lowest address and top is the highest.
701+
#[cfg(all(not(miri), windows))]
702+
fn get_stack_bounds() -> (usize, usize) {
703+
use windows_sys::Win32::System::Threading::{
704+
GetCurrentThreadStackLimits, SetThreadStackGuarantee,
705+
};
706+
let mut low: usize = 0;
707+
let mut high: usize = 0;
708+
unsafe {
709+
GetCurrentThreadStackLimits(&mut low as *mut usize, &mut high as *mut usize);
710+
// Add the guaranteed stack space (reserved for exception handling)
711+
let mut guarantee: u32 = 0;
712+
SetThreadStackGuarantee(&mut guarantee);
713+
low += guarantee as usize;
714+
}
715+
(low, high)
716+
}
717+
718+
/// Get stack boundaries on non-Windows platforms.
719+
/// Falls back to estimating based on current stack pointer.
720+
#[cfg(all(not(miri), not(windows)))]
721+
fn get_stack_bounds() -> (usize, usize) {
722+
// Use pthread_attr_getstack on platforms that support it
723+
#[cfg(any(target_os = "linux", target_os = "android"))]
724+
{
725+
use libc::{
726+
pthread_attr_destroy, pthread_attr_getstack, pthread_attr_t, pthread_getattr_np,
727+
pthread_self,
728+
};
729+
let mut attr: pthread_attr_t = unsafe { std::mem::zeroed() };
730+
unsafe {
731+
if pthread_getattr_np(pthread_self(), &mut attr) == 0 {
732+
let mut stack_addr: *mut libc::c_void = std::ptr::null_mut();
733+
let mut stack_size: libc::size_t = 0;
734+
if pthread_attr_getstack(&attr, &mut stack_addr, &mut stack_size) == 0 {
735+
pthread_attr_destroy(&mut attr);
736+
let base = stack_addr as usize;
737+
let top = base + stack_size;
738+
return (base, top);
739+
}
740+
pthread_attr_destroy(&mut attr);
741+
}
742+
}
743+
}
744+
745+
#[cfg(target_os = "macos")]
746+
{
747+
use libc::{pthread_get_stackaddr_np, pthread_get_stacksize_np, pthread_self};
748+
unsafe {
749+
let thread = pthread_self();
750+
let stack_top = pthread_get_stackaddr_np(thread) as usize;
751+
let stack_size = pthread_get_stacksize_np(thread);
752+
let stack_base = stack_top - stack_size;
753+
return (stack_base, stack_top);
754+
}
755+
}
756+
757+
// Fallback: estimate based on current SP and a default stack size
758+
#[allow(unreachable_code)]
759+
{
760+
let current_sp = psm::stack_pointer() as usize;
761+
// Assume 8MB stack, estimate base
762+
let estimated_size = 8 * 1024 * 1024;
763+
let base = current_sp.saturating_sub(estimated_size);
764+
let top = current_sp + 1024 * 1024; // Assume we're not at the very top
765+
(base, top)
766+
}
767+
}
768+
769+
/// Calculate the C stack soft limit based on actual stack boundaries.
770+
/// soft_limit = base + 2 * margin (for downward-growing stacks)
771+
#[cfg(not(miri))]
772+
fn calculate_c_stack_soft_limit() -> usize {
773+
let (base, _top) = Self::get_stack_bounds();
774+
// Soft limit is 2 margins above the base
775+
base + Self::STACK_MARGIN_BYTES * 2
776+
}
777+
778+
/// Miri doesn't support inline assembly, so disable C stack checking.
779+
#[cfg(miri)]
780+
fn calculate_c_stack_soft_limit() -> usize {
781+
0
782+
}
783+
784+
/// Check if we're near the C stack limit (like _Py_MakeRecCheck).
785+
/// Returns true only when stack pointer is in the "danger zone" between
786+
/// soft_limit and hard_limit (soft_limit - 2*margin).
787+
#[cfg(not(miri))]
788+
#[inline(always)]
789+
fn check_c_stack_overflow(&self) -> bool {
790+
let current_sp = psm::stack_pointer() as usize;
791+
let soft_limit = self.c_stack_soft_limit.get();
792+
// Stack grows downward: check if we're below soft limit but above hard limit
793+
// This matches CPython's _Py_MakeRecCheck behavior
794+
current_sp < soft_limit
795+
&& current_sp >= soft_limit.saturating_sub(Self::STACK_MARGIN_BYTES * 2)
796+
}
797+
798+
/// Miri doesn't support inline assembly, so always return false.
799+
#[cfg(miri)]
800+
#[inline(always)]
801+
fn check_c_stack_overflow(&self) -> bool {
802+
false
803+
}
804+
692805
/// Used to run the body of a (possibly) recursive function. It will raise a
693806
/// RecursionError if recursive functions are nested far too many times,
694807
/// preventing a stack overflow.
695808
pub fn with_recursion<R, F: FnOnce() -> PyResult<R>>(&self, _where: &str, f: F) -> PyResult<R> {
696809
self.check_recursive_call(_where)?;
810+
811+
// Native stack guard: check C stack like _Py_MakeRecCheck
812+
if self.check_c_stack_overflow() {
813+
return Err(self.new_recursion_error(_where.to_string()));
814+
}
815+
697816
self.recursion_depth.set(self.recursion_depth.get() + 1);
698817
let result = f();
699818
self.recursion_depth.set(self.recursion_depth.get() - 1);

crates/vm/src/vm/thread.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ impl VirtualMachine {
234234
state: self.state.clone(),
235235
initialized: self.initialized,
236236
recursion_depth: Cell::new(0),
237+
c_stack_soft_limit: Cell::new(VirtualMachine::calculate_c_stack_soft_limit()),
237238
async_gen_firstiter: RefCell::new(None),
238239
async_gen_finalizer: RefCell::new(None),
239240
};

0 commit comments

Comments
 (0)