Repository navigation
Rak v0.8.4
0.8.4 — 2026-10-03
Security and performance release. This one should be read before upgrading, not
after.
Security
FFI pointer provenance — an arbitrary write anywhere in the process is closed.
ffi_write and ffi_read did this:
unsafe { *((ptr as usize + off) as *mut u8) = byte }with no check that ptr was ever allocated, and none that off was inside it.
Because ffi_ptr(n) builds a pointer from any integer,
ffi_write(ffi_ptr(ADDRESS), 0, 0x41) could write a byte anywhere the process
could reach. ffi_read was the matching read. This is the most severe item in the
security review, and unlike most of that list it was completely real.
A pointer is now provenanced if Rak allocated it (ffi_alloc,
ffi_string_to_cstr) or was told it owns a region (ffi_trust). Provenanced
pointers are range-checked on every access against the size recorded at allocation.
A pointer that is neither is refused by name, and the error says what to do about
it.
ffi_trust is the deliberate escape hatch. A language with FFI that refused every
foreign address would be useless, and resolving a symbol's address and then calling
it is legitimate work. What is not acceptable is an address that is silently
accepted, because that is indistinguishable from a safety check that always passes.
ffi_trust turns unchecked pointer arithmetic into an assertion written down in the
source — the same bargain unsafe { reason } makes everywhere else. It also
narrows: trusting 4 bytes of a 16-byte allocation makes an access at offset 4 fail
again, which is tested.
ffi_cstr_to_string was a denial of service by another route — it scanned for a NUL
byte with no bound, so a pointer to a NUL-free buffer walked off the end into
unmapped memory, reachable from a pointer that came out of a network response. It
is now bounded at 1 MiB.
ffi_read_i32 checks all four bytes, not merely that off is inside: off being
valid while the read runs three bytes past the end is still a read past the end.
The bounds arithmetic is done in u128, not u64, on purpose. ptr + off in u64
wraps, and a bounds check a large offset can defeat is not a check. There is a test
that specifically tries to defeat it.
Both backends share rak_stdlib::ffi::Allocations so the logic lives in one tested
place. ffi_trust is covered by the existing ffi_ capability prefix, so it needs
no new sandbox entry.
Existing ffi_alloc/ffi_free and legitimate access are unchanged and tested.
Performance
Constant folding. compile_expr had none. Every 2 * 3 became two LoadConsts,
an AddI and a push, on every evaluation — inside the innermost loop of every
numeric program. fold_int_binary now folds the literal-on-literal case for the
arithmetic and bitwise operators, and is deliberately narrow: both operands must be
literals; division or remainder by zero, out-of-range shift counts, and overflow all
fold to nothing so the runtime behaviour is unchanged.
rakc bench now measures something. The old version timed
rakc::eval(&source) against vm.run() and nothing else. That is not a backend
comparison: the interpreter figure included lexing, parsing and setup, the VM figure
was bytecode execution on an already-compiled chunk, and compile time was attributed
to nobody. It also used as_millis(), so anything under a millisecond printed
0 ms.
It now reports five phases — lex, parse, compile, interp, vm — each over --repeat
samples (default 5) after an unmeasured warm-up, as a median. The two execution
numbers are finally like for like, and the output labels them "execution only" so
they cannot be misread. On examples/bench.rak in a debug build it reports 6.61x,
which is a measured number where the README previously had a hand-written estimate.
A benchmark corpus. examples/bench/ has six workloads covering recursion,
arithmetic, arrays, map fields, strings and bytes.
Fixes
Three backend message divergences, all the same defect — the VM and the interpreter
described the same failure differently, so an error message told you which backend
you were on:
| expression | interpreter | VM (before) |
|---|---|---|
5 % 0 |
Division by zero |
rem by zero |
5 / 0 |
Division by zero |
div by zero |
n * x |
Undefined variable: x |
Undefined: x |
The first two were Rust's internal panic wording copied into hand-written Err
strings. The interpreter was itself inconsistent on the third (Undefined variable:
in two places, Undefined: in a third); both backends are now normalised to the
clearer form.
Upgrade notes
If you use FFI, you may need to add ffi_trust calls. Any ffi_write/ffi_read
through a pointer that did not come from ffi_alloc or ffi_string_to_cstr will now
fail at the point of use rather than corrupting memory silently — which is the
intended behaviour, but it is a behavioural change.
Release notes are generated from .github/release_body.md at tag time.