reorganize module

This commit is contained in:
Timur Gordon
2025-04-04 08:28:07 +02:00
parent 1ea37e2e7f
commit 939b6b4e57
375 changed files with 7580 additions and 191 deletions

View File

@@ -0,0 +1,98 @@
Turning Off Safety Checks
=========================
{{#include ../links.md}}
Checked Arithmetic
------------------
```admonish bug.side "Don't Panic"
Scripts under normal builds of Rhai never crash the host system – any panic is a bug.
```
By default, all arithmetic calculations in Rhai are _checked_, meaning that the script terminates
with a runtime error whenever it detects a numeric over-flow/under-flow condition or an invalid
floating-point operation.
This checking can be turned off via the [`unchecked`] feature for higher performance (but higher
risks as well).
```rust
let x = 1_000_000_000_000;
x * x; // Normal build - runtime error: multiplication overflow
x * x; // 'unchecked' debug build - panic!
// 'unchecked' release build - overflow with no error
x / 0; // Normal build - runtime error: division by zero
x / 0; // 'unchecked' build - panic!
```
Other Safety Checks
-------------------
In addition to overflows, there are many other safety checks performed by Rhai at runtime.
[`unchecked`] turns them **all** off as well, such as...
### [Infinite loops][maximum number of operations]
```rust
// Normal build - runtime error: exceeds maximum number of operations
loop {
foo();
}
// 'unchecked' build - never terminates!
loop {
foo();
}
```
### [Infinite recursion][maximum call stack depth]
```rust
fn foo() {
foo();
}
foo(); // Normal build - runtime error: exceeds maximum stack depth
foo(); // 'unchecked' build - panic due to stack overflow!
```
### [Gigantic data structures][maximum size of arrays]
```rust
let x = [];
// Normal build - runtime error: array exceeds maximum size
loop {
x += 42;
}
// 'unchecked' build - panic due to out-of-memory!
loop {
x += 42;
}
```
### Improper range iteration
```rust
// Normal build - runtime error: zero step
for x in range(0, 10, 0) { ... }
// 'unchecked' build - never terminates!
for x in range(0, 10, 0) { ... }
// Normal build - empty range
for x in range(0, 10, -1) { ... }
// 'unchecked' build - panic due to numeric underflow!
for x in range(0, 10, -1) { ... }
```

View File

@@ -0,0 +1,115 @@
Safety and Protection Against DOS Attacks
=========================================
{{#title Safety and Protection}}
{{#include ../links.md}}
For scripting systems open to untrusted user-land scripts, it is always best to limit the amount of
resources used by a script so that it does not consume more resources that it is allowed to.
These are common vectors for [Denial of Service (DOS)][DOS] attacks.
Most Important Resources
------------------------
```admonish bug "Memory"
* Continuously grow a [string], an [array], a [BLOB] or [object map] until all memory is consumed.
* Continuously create new [variables] with large data until all memory is consumed.
* Continuously define new [functions] all memory is consumed (e.g. a simple [closure] `||`,
as short as two characters, is a [function] – an attractive target for DOS attacks).
```
```admonish bug "CPU"
* Infinite tight loop that consumes all CPU cycles.
```
```admonish bug "Time"
* Run indefinitely, thereby blocking the calling system which is waiting for a result.
```
```admonish bug "Stack"
* Deep recursive call that exhausts the call stack.
* Large [array] or [object map] literal that exhausts the stack during parsing.
* Degenerated deep expression with so many levels that the parser exhausts the call stack when
parsing the expression; or even deeply-nested statements blocks, if nested deep enough.
* [Self-referencing module][`import`].
```
```admonish bug "Overflows or Underflows"
* Numeric overflows and/or underflows.
* Divide by zero.
* Bad floating-point representations.
```
```admonish bug "Files"
* Continuously [`import`] an external [module] within an infinite loop, thus putting heavy load on the file-system
(or even the network if the file is not local).
Even when [modules] are not created from files, they still typically consume a lot of resources to load.
```
```admonish bug "Private data"
* Read from and/or write to private, secret, sensitive data.
Such security breach may put the entire system at risk.
```
~~~admonish bug "The [`internals`] feature"
The [`internals`] feature allows third-party access to Rust internal data types and functions (for
example, the [`AST`] and related types).
This is usually a _Very Bad Idea™_ because:
* Messing up Rhai's internal data structures will easily create panics that bring down the host
environment, violating the _Don't Panic_ guarantee.
* Allowing access to internal types may open up new attack vectors.
* Internal Rhai types and functions are volatile, so they may change from version to version and
break code.
Use [`internals`] only if the operating environment has absolutely no safety concerns – you'd
be surprised under how few scenarios this assumption holds.
One example of such an environment is a Rhai scripting [`Engine`] compiled to [WASM] where the
[`AST`] is further translated to include environment-specific modifications.
~~~
_Don't Panic_ Guarantee – Any Panic is a Bug
--------------------------------------------------
![Don't
Panic](https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/Don%27t_Panic.svg/320px-Don%27t_Panic.svg.png)
Rhai is designed to not bring down the host system, regardless of what a script may do to it. This
is a central design goal – Rhai provides a [_Don't
Panic_](https://en.wikipedia.org/wiki/Phrases_from_The_Hitchhiker%27s_Guide_to_the_Galaxy#Don't_Panic)
guarantee.
When using Rhai, any panic outside of APIs with explicitly documented panic conditions is
considered a bug in Rhai and should be reported as such.
```admonish tip.small "OK, panic anyway"
All these safe-guards can be turned off via the [`unchecked`] feature, which disables all safety
checks (even fatal ones).
This increases script evaluation performance somewhat, but very easy for a malicious script
to bring down the host system.
```

View File

@@ -0,0 +1,10 @@
Built-In Safety Limits
======================
{{#include ../links.md}}
Rhai has a number of safety limits built into the [`Engine`].
All these limits can be disabled, for higher performance (but higher risks as well), via the
[`unchecked`] feature.

View File

@@ -0,0 +1,63 @@
Maximum Size of Arrays
======================
{{#include ../links.md}}
Rhai by default does not limit how large an [array] or a [BLOB] can be.
This can be changed via the [`Engine::set_max_array_size`][options] method, with zero being
unlimited (the default).
A script attempting to create an [array] literal larger than the maximum will terminate with a parse error.
Any script operation that produces an [array] or a [BLOB] larger than the maximum also terminates
the script with an error result.
This check can be disabled via the [`unchecked`] feature for higher performance (but higher risks as well).
```rust
let mut engine = Engine::new();
engine.set_max_array_size(500); // allow arrays only up to 500 items
engine.set_max_array_size(0); // allow unlimited arrays
```
~~~admonish danger "Maximum size"
Be conservative when setting a maximum limit and always consider the fact that a registered function
may grow an [array]'s or [BLOB]'s size without Rhai noticing until the very end.
For instance, the built-in `+` operator for [arrays] and [BLOB's] concatenates two of them together
to form one larger [array] or [BLOB]; if both sources are _slightly_ below the maximum size limit,
the result may be almost _twice_ the maximum size.
As a malicious script may also create a deeply-nested [array] which consumes huge amounts of memory
while each individual [array] still stays under the maximum size limit, Rhai also _recursively_ adds
up the sizes of all [strings], [arrays], [blobs] and [object maps] contained within each [array] to
make sure that the _aggregate_ sizes of none of these data structures exceed their respective
maximum size limits (if any).
```rust
// Small, innocent array...
let small_array = [42]; // 1-deep... 1 item, 1 array
// ... becomes huge when multiplied!
small_array.push(small_array); // 2-deep... 2 items, 2 arrays
small_array.push(small_array); // 3-deep... 4 items, 4 arrays
small_array.push(small_array); // 4-deep... 8 items, 8 arrays
small_array.push(small_array); // 5-deep... 16 items, 16 arrays
:
:
small_array.push(small_array); // <- Rhai raises an error somewhere here
small_array.push(small_array); // when the TOTAL number of items in
small_array.push(small_array); // the entire array tree exceeds limit
// Or this abomination...
let a = [ 42 ];
loop {
a[0] = a; // <- only 1 item, but infinite number of arrays
}
```
~~~

View File

@@ -0,0 +1,88 @@
Maximum Call Stack Depth
========================
{{#include ../links.md}}
In Rhai, it is trivial for a function call to perform _infinite recursion_ (or a very deeply-nested
recursion) such that all stack space is exhausted.
```rust
// This is a function that, when called, recurses forever.
fn recurse_forever() {
recurse_forever();
}
```
```admonish info.side "Main stack size"
The main stack-size of a program is _not_ determined by Rust but is platform-dependent.
See [this on-line Rust docs](https://doc.rust-lang.org/std/thread/#stack-size) for more details.
```
Because of its intended embedded usage, Rhai, by default, limits function calls to a maximum depth
of 64 levels (8 levels in debug build) in order to fit into most platforms' default stack sizes.
This limit may be changed via the [`Engine::set_max_call_levels`][options] method.
A script exceeding the maximum call stack depth will terminate with an error result.
This check can be disabled via the [`unchecked`] feature for higher performance (but higher risks as well).
```rust
let mut engine = Engine::new();
engine.set_max_call_levels(10); // allow only up to 10 levels of function calls
engine.set_max_call_levels(0); // allow no function calls at all (max depth = zero)
```
```admonish info.small "Additional considerations"
When setting this limit, care must be also be taken to the evaluation depth of each _statement_
within a function.
It is entirely possible for a malicious script to embed a recursive call deep inside a nested
expression or statements block (see [maximum statement depth]).
~~~rust
fn bad_function(n) {
// Bail out long before reaching the limit
if n > 10 {
return;
}
// Nest many, many levels deep...
if check_1() {
if check_2() {
if check_3() {
if check_4() {
:
if check_n() {
bad_function(n+1); // <- recursive call!
}
:
}
}
}
}
}
// The function call below may still overflow the stack!
bad_function(0);
~~~
```
```admonish tip.small "Tip: Getting around the stack size limit"
While the stack size of a program's _main_ thread is platform-specific, Rust defaults to a stack
size of 2MB for spawned threads.
This default can further be changed such that a spawned thread has as large a stack as needed.
See [the on-line Rust docs](https://doc.rust-lang.org/std/thread/#stack-size) for more details.
Therefore, in order to relax the stack size limit for scripts, run the [`Engine`] in a separate
spawned thread with a larger stack.
```

View File

@@ -0,0 +1,24 @@
Maximum Number of Functions
===========================
{{#include ../links.md}}
Rhai by default does not limit how many [functions] can be defined in a script.
This can be changed via the [`Engine::set_max_functions`][options] method. Notice that setting the
maximum number of [functions] to zero does _not_ indicate unlimited [functions], but disallows
defining any scripted [function] altogether.
A script attempting to load more than the maximum number of [functions] will terminate with a parse error.
This check can be disabled via the [`unchecked`] feature for higher performance (but higher risks as well).
```rust
let mut engine = Engine::new();
engine.set_max_functions(5); // allow defining only up to 5 functions
engine.set_max_functions(0); // disallow defining function (maximum = zero)
engine.set_max_functions(1000); // set to a large number for effectively unlimited functions
```

View File

@@ -0,0 +1,64 @@
Maximum Size of Object Maps
===========================
{{#include ../links.md}}
Rhai by default does not limit how large (i.e. the number of properties) an [object map] can be.
This can be changed via `Engine::set_max_map_size`, with zero being unlimited (the default).
A script attempting to create an [object map] literal with more properties than the maximum will
terminate with a parse error.
Any script operation that produces an [object map] with more properties than the maximum also
terminates the script with an error.
This check can be disabled via the [`unchecked`] feature for higher performance (but higher risks as well).
```rust
let mut engine = Engine::new();
engine.set_max_map_size(500); // allow object maps with only up to 500 properties
engine.set_max_map_size(0); // allow unlimited object maps
```
~~~admonish danger "Maximum size"
Be conservative when setting a maximum limit and always consider the fact that a registered function
may grow an [object map]'s size without Rhai noticing until the very end.
For instance, the built-in `+` operator for [object maps] concatenates two [object maps] together to
form one larger [object map]; if both [object maps] are _slightly_ below the maximum size limit, the
resultant [object map] may be almost _twice_ the maximum size.
As a malicious script may create a deeply-nested [object map] which consumes huge amounts of memory
while each individual [object map] still stays under the maximum size limit, Rhai also _recursively_
adds up the sizes of all [strings], [arrays] and [object maps] contained within each [object map] to
make sure that the _aggregate_ sizes of none of these data structures exceed their respective
maximum size limits (if any).
```rust
// Small, innocent object map...
let small_map: #{ x: 42 }; // 1-deep... 1 item, 1 object map
// ... becomes huge when multiplied!
small_map.y = small_map; // 2-deep... 2 items, 2 object maps
small_map.y = small_map; // 3-deep... 4 items, 4 object maps
small_map.y = small_map; // 4-deep... 8 items, 8 object maps
small_map.y = small_map; // 5-deep... 16 items, 16 object maps
:
:
small_map.y = small_map; // <- Rhai raises an error somewhere here
small_map.y = small_map; // when the TOTAL number of items in
small_map.y = small_map; // the entire array tree exceeds limit
// Or this abomination...
let map = #{ x: 42 };
loop {
map.x = map; // <- only 1 item, but infinite number of object maps
}
```
~~~

View File

@@ -0,0 +1,27 @@
Maximum Number of Modules
=========================
{{#include ../links.md}}
Rhai by default does not limit how many [modules] can be loaded via [`import`] statements.
This can be changed via the [`Engine::set_max_modules`][options] method. Notice that setting the
maximum number of modules to zero does _not_ indicate unlimited modules, but disallows loading any
module altogether.
A script attempting to load more than the maximum number of modules will terminate with an error result.
This limit can also be used to stop [`import`-loops][`import`] (i.e. cycles of modules referring to
each other).
This check can be disabled via the [`unchecked`] feature for higher performance (but higher risks as well).
```rust
let mut engine = Engine::new();
engine.set_max_modules(5); // allow loading only up to 5 modules
engine.set_max_modules(0); // disallow loading any module (maximum = zero)
engine.set_max_modules(1000); // set to a large number for effectively unlimited modules
```

View File

@@ -0,0 +1,51 @@
Maximum Number of Operations
============================
{{#include ../links.md}}
In Rhai, it is trivial to construct _infinite loops_, or scripts that run for a very long time.
```rust
loop { ... } // infinite loop
while 1 < 2 { ... } // loop with always-true condition
```
Rhai by default does not limit how much time or CPU a script consumes.
This can be changed via the [`Engine::set_max_operations`][options] method, with zero being
unlimited (the default).
The _operations count_ is intended to be a very course-grained measurement of the amount of CPU that
a script has consumed, allowing the system to impose a hard upper limit on computing resources.
A script exceeding the maximum operations count terminates with an error result. This can be
disabled via the [`unchecked`] feature for higher performance (but higher risks as well).
```rust
let mut engine = Engine::new();
engine.set_max_operations(500); // allow only up to 500 operations for this script
engine.set_max_operations(0); // allow unlimited operations
```
```admonish question "TL;DR &ndash; What does one _operation_ mean?"
The concept of one single _operation_ in Rhai is volatile &ndash; it roughly equals one expression
node, loading one [variable]/[constant], one [operator] call, one iteration of a loop, or one
[function] call etc. with sub-expressions, statements and [function] calls executed inside these
contexts accumulated on top.
A good rule-of-thumb is that one simple non-trivial expression consumes on average 5-10 operations.
One _operation_ can take an unspecified amount of time and real CPU cycles, depending on the particulars.
For example, loading a [constant] consumes very few CPU cycles, while calling an external Rust function,
though also counted as only one operation, may consume much more computing resources.
To help visualize, think of an _operation_ as roughly equals to one _instruction_ of a hypothetical CPU
which includes _specialized_ instructions, such as _function call_, _load module_ etc., each taking up
one CPU cycle to execute.
```

View File

@@ -0,0 +1,71 @@
Maximum Expression Nesting Depth
================================
{{#include ../links.md}}
Rhai by default limits statement and expression nesting to a maximum depth of 64 (which should be
plenty) when they are at _global_ level, but only a depth of 32 when they are within [function] bodies.
For debug builds, these limits are set further downwards to 32 and 16 respectively.
That is because it is possible to overflow the [`Engine`]'s stack when it tries to recursively parse
an extremely deeply-nested code stream.
```rust
// The following, if long enough, can easily cause stack overflow during parsing.
let a = (1+(1+(1+(1+(1+(1+(1+(1+(1+(1+(...)+1)))))))))));
```
This limit may be changed via [`Engine::set_max_expr_depths`][options].
There are two limits to set, one for the maximum depth at global level, and the other for [function] bodies.
A script exceeding the maximum nesting depths will terminate with a parse error. The malicious
[`AST`] will not be able to get past parsing in the first place.
This check can be disabled via the [`unchecked`] feature for higher performance (but higher risks as well).
```rust
let mut engine = Engine::new();
engine.set_max_expr_depths(50, 5); // allow nesting up to 50 layers of expressions/statements
// at global level, but only 5 inside functions
```
```admonish warning
Multiple layers of expressions may be generated for a simple language construct, even though it may correspond
to only one AST node.
That is because the Rhai _parser_ internally runs a recursive chain of [function] calls and it is important that
a malicious script does not panic the parser in the first place.
```
~~~admonish danger "Beware of recursion"
_[Functions]_ are placed under stricter limits because of the multiplicative effect of _recursion_.
A [function] can effectively call itself while deep inside an expression chain within the [function]
body, thereby overflowing the stack even when the level of recursion is within limit.
```rust
fn deep_calc(a, n) {
(a+(a+(a+(a+(a+(a+(a+(a+(a+ ... (a+deep_calc(a,n+1)) ... )))))))))
// ^^^^^^^^^^^^^^^^ recursive call!
}
let a = 42;
let result = (a+(a+(a+(a+(a+(a+(a+(a+(a+ ... (a+deep_calc(a,0)) ... )))))))));
```
In the contrived example above, each recursive call to the [function] `deep_calc` adds the total
number of nested expression layers to Rhai's evaluation stack. Sooner or later (most likely sooner
than the limit for [maximum depth of function calls][maximum call stack depth] is reached), a stack
overflow can be expected.
In general, make sure that `C x ( 5 + F ) + S` layered calls do not cause a stack overflow, where:
* `C` = maximum call stack depth,
* `F` = maximum statement depth for [functions],
* `S` = maximum statement depth at global level.
~~~

View File

@@ -0,0 +1,34 @@
Maximum Length of Strings
=========================
{{#include ../links.md}}
Rhai by default does not limit how long a [string] can be.
This can be changed via the [`Engine::set_max_string_size`][options] method, with zero being
unlimited (the default).
A script attempting to create a [string] literal longer than the maximum length will terminate with
a parse error.
Any script operation that produces a [string] longer than the maximum also terminates the script
with an error.
This check can be disabled via the [`unchecked`] feature for higher performance (but higher risks as well).
```rust
let mut engine = Engine::new();
engine.set_max_string_size(500); // allow strings only up to 500 bytes long (in UTF-8 format)
engine.set_max_string_size(0); // allow unlimited string length
```
```admonish danger.small "Maximum length"
Be conservative when setting a maximum limit and always consider the fact that a registered function may grow
a string's length without Rhai noticing until the very end.
For instance, the built-in `+` operator for strings concatenates two strings together to form one longer string;
if both strings are _slightly_ below the maximum length limit, the resultant string may be almost _twice_ the maximum length.
```

View File

@@ -0,0 +1,54 @@
Maximum Number of Variables
===========================
{{#include ../links.md}}
Rhai by default does not limit how many [variables]/[constants] can be defined within a single [`Scope`].
This can be changed via the [`Engine::set_max_variables`][options] method. Notice that setting the
maximum number of [variables] to zero does _not_ indicate unlimited [variables], but disallows
defining any [variable] altogether.
A script attempting to define more than the maximum number of [variables]/[constants] will terminate
with an error result.
This check can be disabled via the [`unchecked`] feature for higher performance (but higher risks as well).
```rust
let mut engine = Engine::new();
engine.set_max_variables(5); // allow defining only up to 5 variables
engine.set_max_variables(0); // disallow defining any variable (maximum = zero)
engine.set_max_variables(1000); // set to a large number for effectively unlimited variables
```
```admonish warning.small "Function calls are separate scopes"
Each [function] call creates a new, empty [`Scope`].
Therefore, [variables]/[constants] defined within [functions] are counted afresh.
Care must be taken to avoid deeply-nested (or recursive) [function] calls from creating too many
[variables]/[constants] while staying within the limit of each individual [`Scope`].
```
```admonish warning.small "Function call arguments count as variables"
The parameters of a [function] also count as [variables] within the [function]'s [`Scope`].
Thus the maximum number of [variables]/[constants] allowed is reduced by the number of parameters of the [function].
```
~~~admonish tip.small "Tip: Reusing a variable doesn't count"
It is possible to _reuse_ a [variable] such that it is counted only once.
```rust
let x = 42; // counted as 1 variable
let y = 123;
let x = 0; // previous 'x' reused: not counted as new variable
```
~~~

View File

@@ -0,0 +1,62 @@
Limiting Memory Usage
=====================
{{#include ../links.md}}
During Evaluation
-----------------
To prevent _out-of-memory_ failures, provide a closure to [`Engine::on_progress`][progress] to track
memory usage and force-terminate a malicious script before it can bring down the host system.
Most O/S provides system calls to obtain the current memory usage of the process.
```rust
let mut engine = Engine::new();
const MAX_MEMORY: usize = 10 * 1024 * 1024; // 10MB
engine.on_progress(|_| {
// Call a system function to obtain the current memory usage
let memory_usage = get_current_progress_memory_usage();
if memory_usage > MAX_MEMORY {
// Terminate the script
Some(Dynamic::UNIT)
} else {
// Continue
None
}
});
```
During Parsing
--------------
A malicious script can be carefully crafted such that it consumes all available memory during the
parsing stage.
Protect against this by via a closure to [`Engine::on_parse_token`][token remap filter].
```rust
let mut engine = Engine::new();
const MAX_MEMORY: usize = 10 * 1024 * 1024; // 10MB
engine.on_parse_token(|token, _, _| {
// Call a system function to obtain the current memory usage
let memory_usage = get_current_progress_memory_usage();
if memory_usage > MAX_MEMORY {
// Terminate parsing
Token::LexError(
LexError::Runtime("out of memory".into()).into()
)
} else {
// Continue
token
}
});
```

View File

@@ -0,0 +1,99 @@
Limiting Run Time
=================
[`Engine::on_progress`]: https://docs.rs/rhai/{{version}}/rhai/struct.Engine.html#method.on_progress
Track Progress and Force-Termination
------------------------------------
{{#include ../links.md}}
```admonish info.side "Operations count vs. progress"
_Operations count_ does not indicate the _proportion_ of work already done &ndash; thus it is not real _progress_ tracking.
The real progress can be _estimated_ based on the expected number of operations in a typical run.
```
It is impossible to know when, or even whether, a script run will end
(a.k.a. the [Halting Problem](http://en.wikipedia.org/wiki/Halting_problem)).
When dealing with third-party untrusted scripts that may be malicious, in order to track evaluation
progress and force-terminate a script prematurely (for any reason), provide a closure to the
[`Engine`] via [`Engine::on_progress`].
The closure passed to [`Engine::on_progress`] will be called once for every operation.
Progress tracking is disabled with the [`unchecked`] feature.
Examples
--------
### Periodic Logging
```rust
let mut engine = Engine::new();
engine.on_progress(|count| { // parameter is number of operations already performed
if count % 1000 == 0 {
println!("{count}"); // print out a progress log every 1,000 operations
}
None // return 'None' to continue running the script
// return 'Some(token)' to immediately terminate the script
});
```
### Limit running time
```rust
let mut engine = Engine::new();
let start = get_time(); // get the current system time
engine.on_progress(move |_| {
let now = get_time();
if now.duration_since(start).as_secs() > 60 {
// Return a dummy token just to force-terminate the script
// after running for more than 60 seconds!
Some(Dynamic::UNIT)
} else {
// Continue
None
}
});
```
Function Signature of Callback
------------------------------
The signature of the closure to pass to [`Engine::on_progress`] is as follows.
> ```rust
> Fn(operations: u64) -> Option<Dynamic>
> ```
### Return value
| Value | Effect |
| :-----------: | -------------------------------------------------------------------------------- |
| `Some(token)` | terminate immediately, with `token` (a [`Dynamic`] value) as _termination token_ |
| `None` | continue script evaluation |
### Termination Token
```admonish info.side "Token"
The termination token is commonly used to provide information on the _reason_ behind the termination decision.
```
The [`Dynamic`] value returned is a _termination token_.
A script that is manually terminated returns with the error `EvalAltResult::ErrorTerminated(token, position)`
wrapping this value.
If the termination token is not needed, simply return `Some(Dynamic::UNIT)` to terminate the script
run with [`()`] as the token.

View File

@@ -0,0 +1,40 @@
Sand-Boxing &ndash; Block Access to External Data
=================================================
{{#include ../links.md}}
Rhai is _sand-boxed_ so a script can never read from outside its own environment.
Furthermore, an [`Engine`] created non-`mut` cannot mutate any state, including itself
(and therefore it is also _re-entrant_).
It is highly recommended that [`Engine`]'s be created immutable as much as possible.
```rust
let mut engine = Engine::new();
// Use the fluent API to configure an 'Engine'
engine.register_get("field", get_field)
.register_set("field", set_field)
.register_fn("do_work", action);
// Then turn it into an immutable instance
let engine = engine;
// 'engine' is immutable...
```
```admonish tip.small "Tip: Use Rhai to control external environment"
How does a _sand-boxed_, immutable [`Engine`] control the external environment?
This is necessary in order to use Rhai as a _dynamic control layer_ over a Rust core system.
There are two general patterns, both involving wrapping the external system
in a shared, interior-mutated object (e.g. `Rc<RefCell<T>>`):
* [Control Layer]({{rootUrl}}/patterns/control.md) pattern.
* [Singleton Command Object]({{rootUrl}}/patterns/singleton.md) pattern.
```

View File

@@ -0,0 +1,124 @@
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.
```rust
// 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) };
```
```admonish question.small "Negative values"
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.
```rust
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].
```rust
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
}
});
```