This repository has been archived on 2025-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
rhaj/_archive/rhai_engine/rhaibook/safety/stack.md
2025-04-04 08:28:07 +02:00

3.3 KiB

Limiting Stack Usage

{{#include ../links.md}}

Most O/S differentiates between heap and stack memory.

Usually the stack (around 1MB) is much smaller than the heap (multiple MB's or even GB's).

Therefore, it is possible for a carefully-crafted script to consume all available stack memory (such as deeply-nested expressions) and crash the host system, even though there is ample heap memory available.

Calculate Stack Usage

Some O/S's provide system calls to get the stack size and/or amount of free stack memory, but these are in the minority.

In order to determine the amount of stack memory actually used, it is necessary to perform some pointer arithmetic.

The trick is to get the address of a stack-allocated variable in the very beginning and compare it to the address of another variable.

// Create a variable on the stack.
let stack_base_ref = Dynamic::UNIT;
// Get a pointer to it.
let stack_base: *const Dynamic = &stack_base_ref;

// ... do a lot of work here ...

// Create another variable on the stack.
let stack_top = Dynamic::UNIT;
// Get a pointer to it.
let stack_top: *const Dynamic = &stack_top;

let usage = unsafe { stack_top.offset_from(stack_base) };

In many cases, the amount of stack memory used is actually _negative_
(meaning that the base variable is in a higher memory address than the current variable).

That is because, for many architectures, the stack grows _downwards_ and the heap
grows _upwards_ in order to maximize memory usage efficiency.

During Evaluation

To prevent stack-overflow failures, provide a closure to [Engine::on_progress][progress] to track stack usage and force-terminate a malicious script before it can bring down the host system.

let mut engine = Engine::new();

const MAX_STACK: usize = 100 * 1024;    // 10KB

// Create a variable on the stack.
let stack_base_ref = Dynamic::UNIT;
// Get a pointer to it.
let stack_base: *const Dynamic = &stack_base_ref;

engine.on_progress(move |_| {
    // Create another variable on the stack.
    let stack_top = Dynamic::UNIT;
    // Get a pointer to it.
    let stack_top: *const Dynamic = &stack_top;

    let usage = unsafe { stack_base.offset_from(stack_top) };

    if usage > MAX_STACK {
        // Terminate the script 
        Some(Dynamic::UNIT)
    } else {
        // Continue
        None
    }
});

During Parsing

A malicious script can be carefully crafted such that it consumes all stack memory during the parsing stage.

Protect against this by via a closure to [Engine::on_parse_token][token remap filter].

let mut engine = Engine::new();

const MAX_STACK: usize = 100 * 1024;    // 10KB

// Create a variable on the stack.
let stack_base_ref = Dynamic::UNIT;
// Get a pointer to it.
let stack_base: *const Dynamic = &stack_base_ref;

engine.on_parse_token(|token, _, _| {
    // Create another variable on the stack.
    let stack_top = Dynamic::UNIT;
    // Get a pointer to it.
    let stack_top: *const Dynamic = &stack_top;

    let usage = unsafe { stack_base.offset_from(stack_top) };

    if usage > MAX_STACK {
        // Terminate parsing
        Token::LexError(
            LexError::Runtime("stack-overflow".into()).into()
        )
    } else {
        // Continue
        token
    }
});