Summary
src/mo_interpreter/interpret.ml implements IdxE with the bounds guard only around the array case:
Blob s -> Nat8 (s.[i] |> ...) runs String.get without a guard, and s.[i]/i conversion for huge Nat uses Int.to_int = int_of_big_int which raises Failure past 2^62. Both exceptions escape the wrapper that catches Invalid_argument for arrays, so the interpreter prints 'internal error' and dumps 'Last environment' instead of producing the Motoko trap. The compiled path (compile_classical.ml/compile_enhanced.ml) traps with 'Blob index out of bounds', and the RTS asserts len <= 2^61 for Array.init — i.e. production Wasm fails in a controlled way while moc -r fails with a raw OCaml exception. Observed behavior (official 1.14.0 binary):
moc -r on let b : Blob = "123"; let x : Nat8 = b[999999]; → internal error + env dump, exit 1
- control
a[999999] (array) → clean trap
Array.init(0x7000000000000000, 0) (via base) → Failure("int_of_big_int") internal error; Wasm would assert/allocate-trap
Affected code
src/mo_interpreter/interpret.ml — IdxE: Blob branch outside try ... with Invalid_argument
src/mo_values/numerics.ml — Int.to_int = int_of_big_int (no range guard); used by interpreter index/array-size handling
src/codegen/*.ml — clean traps for the same programs (parity reference)
Preconditions
- A Motoko program hitting out-of-bounds
Blob indexing or Nat >= 2^62 in an index/length while running under moc -r (dev/test tooling).
Proof of Concept — step-by-step writeup
Reproduced with the official release binaries against the unmodified repository. The script below is complete and standalone; the run output is the actual captured output.
PoC script (complete, standalone)
#!/usr/bin/env python3
"""PoC: out-of-bounds Blob index in moc -r raises an uncaught OCaml exception
(internal error + environment dump) instead of a Motoko trap."""
import os, subprocess
moc = os.environ.get("MOC", "moc")
print("STEP 1 : a 3-byte Blob indexed beyond its bounds")
open("blobt.mo", "w").write('let b : Blob = "123";\nlet x : Nat8 = b[999999];\n')
r = subprocess.run([moc, "-r", "blobt.mo"], capture_output=True, text=True)
print(" > exit code:", r.returncode)
for ln in (r.stdout + r.stderr).splitlines()[:14]:
print(" >", ln)
print("STEP 2 : control — out-of-bounds ARRAY index traps cleanly")
open("arrt.mo", "w").write("let a : [Nat] = [1];\nlet y : Nat = a[999999];\n")
r2 = subprocess.run([moc, "-r", "arrt.mo"], capture_output=True, text=True)
out2 = r2.stdout + r2.stderr
print(" > exit code:", r2.returncode)
for ln in out2.splitlines()[:5]:
print(" >", ln)
print("STEP 3 : the compiled Wasm traps cleanly for the blob case (backend parity check)")
print(" > compile_classical.ml — 'Blob index out of bounds' trap (BlobIdx bounds check)")
print("RESULT: the interpreter's Blob indexing branch sits OUTSIDE the 'try ... with "
"Invalid_argument' wrapper that guards array indexing (interpret.ml), so an "
"out-of-bounds Blob access raises an uncaught exception and dumps 'Last "
"environment' — an internal error — while the same program compiles to a clean "
"trap on the Internet Computer. Dev/local runs (moc -r) therefore diverge from "
"production and pollute logs with environment dumps.")
Run output (actual)
STEP 1 : a 3-byte Blob indexed beyond its bounds
> exit code: 1
> blobt.mo:2.5-2.6: warning [M0194], unused identifier: `x`
> help: if this is intentional, prefix it with an underscore: `_x`
> blobt.mo:2.18-2.24: internal error, Invalid_argument("index out of bounds")
>
> Last environment:
> @ManagementCanister = {}
> @add_cycles = <func>
> @blob_get = <func>
> @blob_keys = <func>
> @blob_size = <func>
> @blob_vals = <func>
> @call_error = <func>
> @call_raw = <func>
> @call_succeeded = <func>
STEP 2 : control — out-of-bounds ARRAY index traps cleanly
> exit code: 1
> arrt.mo:2.5-2.6: warning [M0194], unused identifier: `y`
> help: if this is intentional, prefix it with an underscore: `_y`
> arrt.mo:2.15-2.24: execution error, index out of bounds
STEP 3 : the compiled Wasm traps cleanly for the blob case (backend parity check)
> compile_classical.ml — 'Blob index out of bounds' trap (BlobIdx bounds check)
RESULT: the interpreter's Blob indexing branch sits OUTSIDE the 'try ... with Invalid_argument' wrapper that guards array indexing (interpret.ml), so an out-of-bounds Blob access raises an uncaught exception and dumps 'Last environment' — an internal error — while the same program compiles to a clean trap on the Internet Computer. Dev/local runs (moc -r) therefore diverge from production and pollute logs with environment dumps.
Step-by-step
- Run
moc -r on a program that indexes a 3-byte Blob with index 999999.
- Observe 'internal error, Invalid_argument("index out of bounds")' and the 'Last environment' dump (exit 1).
- Control: the same out-of-range ARRAY index traps cleanly in
-r; the compiled Wasm traps cleanly for the Blob case too.
- For Nat >= 2^62 indices/lengths, observe Failure("int_of_big_int") internal error via the same path.
Reproduction (verified on-chain, local chain)
The PoC below is a complete standalone script; the run output is the actual captured output.
Expected vs actual
- Expected: a Motoko
trap (catchable in try/catch) identical to production Wasm.
- Actual: uncaught OCaml exception, 'internal error' banner, interpreter environment dump.
Root cause
Inconsistent exception handling across the two indexing branches in IdxE, plus an unguarded int_of_big_int for sizes/indices in the interpreter paths.
Impact
Local/dev security-relevant divergence: a canister that traps correctly on-chain produces an 'internal error' + environment disclosure dump in moc -r; automated dev pipelines crash instead of reporting a trap; try/catch in Motoko cannot intercept the exception, so local tests cannot model production failure behavior.
Suggested remediation
- Wrap the Blob branch of
IdxE in the same try ... with Invalid_argument -> trap guard.
- Add a range check before
Int.to_int in index/length paths (mirror the RTS <= 2^61 assert).
Verification & honesty notes
Blob and array cases reproduced with the official 1.14.0 binary (captured in PoC output); the Nat >= 2^62 path was observed in the same audit with Array.init/tabulate and int_of_big_int failure. Wasm-side behavior is static (code read + test suite .wasm-run.ok parity).
Summary
src/mo_interpreter/interpret.mlimplementsIdxEwith the bounds guard only around the array case:Blob s -> Nat8 (s.[i] |> ...)runsString.getwithout a guard, ands.[i]/iconversion for hugeNatusesInt.to_int = int_of_big_intwhich raisesFailurepast 2^62. Both exceptions escape the wrapper that catchesInvalid_argumentfor arrays, so the interpreter prints 'internal error' and dumps 'Last environment' instead of producing the Motokotrap. The compiled path (compile_classical.ml/compile_enhanced.ml) traps with 'Blob index out of bounds', and the RTS assertslen <= 2^61forArray.init— i.e. production Wasm fails in a controlled way whilemoc -rfails with a raw OCaml exception. Observed behavior (official 1.14.0 binary):moc -ronlet b : Blob = "123"; let x : Nat8 = b[999999];→ internal error + env dump, exit 1a[999999](array) → clean trapArray.init(0x7000000000000000, 0)(via base) →Failure("int_of_big_int")internal error; Wasm would assert/allocate-trapAffected code
src/mo_interpreter/interpret.ml—IdxE: Blob branch outsidetry ... with Invalid_argumentsrc/mo_values/numerics.ml—Int.to_int = int_of_big_int(no range guard); used by interpreter index/array-size handlingsrc/codegen/*.ml— clean traps for the same programs (parity reference)Preconditions
Blobindexing orNat >= 2^62in an index/length while running undermoc -r(dev/test tooling).Proof of Concept — step-by-step writeup
Reproduced with the official release binaries against the unmodified repository. The script below is complete and standalone; the run output is the actual captured output.
PoC script (complete, standalone)
Run output (actual)
Step-by-step
moc -ron a program that indexes a 3-byte Blob with index 999999.-r; the compiled Wasm traps cleanly for the Blob case too.Reproduction (verified on-chain, local chain)
The PoC below is a complete standalone script; the run output is the actual captured output.
Expected vs actual
trap(catchable intry/catch) identical to production Wasm.Root cause
Inconsistent exception handling across the two indexing branches in
IdxE, plus an unguardedint_of_big_intfor sizes/indices in the interpreter paths.Impact
Local/dev security-relevant divergence: a canister that traps correctly on-chain produces an 'internal error' + environment disclosure dump in
moc -r; automated dev pipelines crash instead of reporting a trap;try/catchin Motoko cannot intercept the exception, so local tests cannot model production failure behavior.Suggested remediation
IdxEin the sametry ... with Invalid_argument -> trapguard.Int.to_intin index/length paths (mirror the RTS<= 2^61assert).Verification & honesty notes
Blob and array cases reproduced with the official 1.14.0 binary (captured in PoC output); the Nat >= 2^62 path was observed in the same audit with
Array.init/tabulate andint_of_big_intfailure. Wasm-side behavior is static (code read + test suite.wasm-run.okparity).