Skip to content

A missing argument to a native method still surfaces Go's panic text as the Ruby message — the half of #777 that was left open #848

Open

Description

@tannevaled

The leak #777 closed for allocation faults is still open for missing arguments

callNative (internal/vm/object_model.go:1558) converts a Go runtime fault into
a rescuable Ruby ArgumentError so that, as its comment says, "arbitrary input
from an embedding host — the wasm playground REPL, say — can never crash the
process"
. That net is right and should stay.

What it still does is hand the Go panic text to Ruby code as the exception
message
. The same comment records this, for the allocation case:

Reporting it as ArgumentError also leaked the Go text verbatim into a Ruby
message, which is how "x".ljust(2**62) came to raise
ArgumentError: ljust: runtime error: makeslice: len out of range where MRI
4.0.5 raises NoMemoryError: failed to allocate memory (issue #777).

#777 fixed the allocation class: isAllocFault recognises makeslice:,
makemap: and growslice: and raises NoMemoryError with MRI's own literal.
The index-out-of-range class — a binding reading args[0] that was never
passed — was explicitly left as "the pre-existing behaviour (callNative's
ArgumentError)", and it is still there.

Measured, on main (3a3fb81), darwin/arm64

Eight calls with a missing or extra argument, in one run.

⛔ The rbgo column is measured; the MRI column is not. No MRI is invocable on
this machine, so the right-hand side is MRI's documented arity rendered in its
standard message form, not a reading. It should be re-measured against the
oracle before any of it is used as an expected value in a test — the point of
the table is the left column, where every row was produced by running the
command.

call rbgo (measured) MRI 4.0 (documented arity, NOT measured here)
"x".center() ArgumentError: center: runtime error: index out of range [0] with length 0 ArgumentError: wrong number of arguments (given 0, expected 1..2)
1.divmod() ArgumentError: divmod: runtime error: index out of range [0] with length 0 ArgumentError: wrong number of arguments (given 0, expected 1)
"a".sub() ArgumentError: sub: runtime error: index out of range [0] with length 0 ArgumentError: wrong number of arguments (given 0, expected 1..2)
Integer() ArgumentError: Integer: runtime error: index out of range [0] with length 0 ArgumentError: wrong number of arguments (given 0, expected 1..2)
Math.cbrt ArgumentError: cbrt: runtime error: index out of range [0] with length 0 ArgumentError: wrong number of arguments (given 0, expected 1)
[1].fetch() ArgumentError: wrong number of arguments (given 0, expected 1..2) ✅ same
{}.fetch() ArgumentError: wrong number of arguments (given 0, expected 1..2) ✅ same
[1,2].each_slice() ArgumentError: wrong number of arguments (given 0, expected 1) ✅ same

So some natives declare their arity and some do not, and the ones that do not
fall through to the net. Five of the eight probed are wrong; three are right.
This is a sample I picked, not a census — the real count needs a sweep.

One more shape, wrong in the other direction — and this row needs no reference
to be a defect, because answering an over-supplied call with a value rather than
an error is wrong against any Ruby:

| [1,2].first(1,2,3) | no error at all, returns a value | an ArgumentError |

The three ✅ rows are the useful control: they prove the correct message is
reachable through the same dispatch, so the five wrong ones are not a limitation
of the architecture.

Why it matters, stated without inflation

It is not an information leak today. The text names a Go method, says
"runtime error", and gives an index and a length both derived from the call
([0] with length 0) — nothing secret. The problem is the mechanism, which is
exactly the one #777 showed can carry internals outward: that issue's leak
exposed Go's allocator wording for a size the program chose.

The concrete harm now is smaller and certain:

  • The message is wrong, where this project's whole discipline is MRI's exact
    messages. The Math::DomainError and TypeError strings next door are
    measured against MRI character by character.
  • It is not stable. "index out of range [0] with length 0" is Go's wording,
    and a Ruby program that rescues and matches on it is matching on our build and
    our Go version. The project already guards against that shape of coupling in
    convertAllocPanic, which warns that if Go renames its prefixes the
    classification degrades and a test says so.
  • It reports a bad call as a runtime fault, so the backstop that exists to
    keep a host alive is also the thing answering an ordinary arity mistake.

Suggested direction

The net stays; what reaches it changes. Natives should declare their arity, as
Array#fetch, Hash#fetch and Enumerable#each_slice evidently already do, so
callNative's recover becomes what its comment says it is — a backstop for
genuine faults rather than the normal path for a missing argument.

If that is too broad for one change, the ordering that worked for #777 applies:
pick the class with no behavioural ambiguity first. Nobody depends on
"index out of range [0] with length 0", so replacing it with MRI's wording
cannot break a program that was relying on the old answer — unlike the
extra-argument half (#847), where a call that silently succeeded today would
start raising.

Math is the instance that led here and is filed separately as #847;
#846 has the two new 4.0 functions, which check their own arity for this reason
and pin MRI's wording in a test so they cannot drift into the leak.

🤖 Generated with Claude Code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    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