Skip to content

Interpreter internal error instead of a trap: out-of-bounds Blob indexing (b[999999]) raises an uncaught OCaml exception and dumps the environment, diverging from the compiled Wasm which traps cleanly #6308

Description

@haoxucu

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

  1. Run moc -r on a program that indexes a 3-byte Blob with index 999999.
  2. Observe 'internal error, Invalid_argument("index out of bounds")' and the 'Last environment' dump (exit 1).
  3. Control: the same out-of-range ARRAY index traps cleanly in -r; the compiled Wasm traps cleanly for the Blob case too.
  4. 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions