Skip to content

Bug: class constructor panics with "index out of bounds: the len is 0 but the index is 0" #5351

@acsses

Description

@acsses

Bug Report

Description

Executing JavaScript code containing class constructors with const declarations
causes a panic in boa_engine:
thread 'main' panicked at 'index out of bounds: the len is 0 but the index is 0'
boa_engine/src/environments/runtime/declarative/mod.rs:310

Stack trace

6: boa_engine::environments::runtime::declarative::PoisonableEnvironment::set
at boa_engine-0.21.1/src/environments/runtime/declarative/mod.rs:310
7: boa_engine::environments::runtime::declarative::function::FunctionEnvironment::set
at boa_engine-0.21.1/src/environments/runtime/declarative/function.rs:66
10: boa_engine::environments::runtime::EnvironmentStack::put_lexical_value
at boa_engine-0.21.1/src/environments/runtime/mod.rs:280
11: boa_engine::vm::opcode::define::PutLexicalValue::operation
at boa_engine-0.21.1/src/vm/opcode/define/mod.rs:82

Minimal reproduction

// Case 1: arguments binding
class A {
  constructor() {
    const x = 1;
  }
}
new A();

// Case 2: class constructor with closure over non-local binding
var B = class {
  constructor(components) {
    const { getHasher } = components;
    this.fn = () => getHasher("sha2-256");
  }
};
new B({ getHasher: (x) => x });

Expected behavior

Both cases execute without panic.

Actual behavior

Panic at runtime: index out of bounds: the len is 0 but the index is 0

Root cause analysis

Two bugs were identified:

Bug 1 (boa_ast/src/scope.rs):

scope_analyzer always creates an arguments binding in the function scope
without the ESCAPES flag. num_bindings_non_local() only counts bindings
with ESCAPES=true, so arguments is excluded from the slot count.
FunctionEnvironment is created with 0 slots, but the bytecode still emits
PutLexicalValue(index=0) for it, causing a panic.

Bug 2 (boa_engine/src/bytecompiler/class.rs):

compile_class() always calls push_scope() once for the constructor's
function scope, placing it at constant_scope(0). However, scope_analyzer
assigns scope indices via visit_function_like, which increments self.index
before calling function_scope.set_index(). This causes a mismatch between
the constant_scope index and the scope index expected at runtime, resulting
in the wrong (or nonexistent) scope being accessed.

FunctionCompiler in function.rs handles this correctly by mirroring the
scope index assignment, but compile_class() did not follow the same logic.

Version

boa_engine 0.21.1

Additional context

This bug was triggered by esbuild-bundled JavaScript code with --target=es2020,
which transforms class fields into __publicField() helper calls. The combination
of class constructors with const declarations and closures exposed both bugs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions