reorganize module
This commit is contained in:
@@ -1,98 +0,0 @@
|
||||
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) { ... }
|
||||
```
|
@@ -1,115 +0,0 @@
|
||||
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
|
||||
--------------------------------------------------
|
||||
|
||||

|
||||
|
||||
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.
|
||||
```
|
@@ -1,10 +0,0 @@
|
||||
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.
|
@@ -1,63 +0,0 @@
|
||||
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
|
||||
}
|
||||
```
|
||||
~~~
|
@@ -1,88 +0,0 @@
|
||||
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.
|
||||
```
|
@@ -1,24 +0,0 @@
|
||||
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
|
||||
```
|
@@ -1,64 +0,0 @@
|
||||
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
|
||||
}
|
||||
```
|
||||
~~~
|
@@ -1,27 +0,0 @@
|
||||
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
|
||||
```
|
@@ -1,51 +0,0 @@
|
||||
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 – What does one _operation_ mean?"
|
||||
|
||||
The concept of one single _operation_ in Rhai is volatile – 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.
|
||||
```
|
@@ -1,71 +0,0 @@
|
||||
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.
|
||||
~~~
|
@@ -1,34 +0,0 @@
|
||||
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.
|
||||
```
|
@@ -1,54 +0,0 @@
|
||||
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
|
||||
```
|
||||
~~~
|
@@ -1,62 +0,0 @@
|
||||
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
|
||||
}
|
||||
});
|
||||
```
|
@@ -1,99 +0,0 @@
|
||||
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 – 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.
|
@@ -1,40 +0,0 @@
|
||||
Sand-Boxing – 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.
|
||||
```
|
@@ -1,124 +0,0 @@
|
||||
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
|
||||
}
|
||||
});
|
||||
```
|
Reference in New Issue
Block a user