Skip to content

Sloppy o.x += 1 on a frozen object over-throws: Expr::PropertySet carries no strictness field at all #9459

Description

@proggeramlug
// sloppy (.cjs, no "use strict")
const o = {x:1}; Object.freeze(o);
o.x += 1;              // node: silent no-op    perry: TypeError
o.x++;                 // node: silent          perry: silent      ✓
for (o.x of [7]) {}    // node: silent          perry: TypeError
[o.x] = [7];           // node: silent          perry: TypeError

Over-throwing, not under-throwing — the mirror image of #9394 and #9422, on the object path.

+= lowers to Expr::PropertySet, which carries no strictness field at all, and its codegen reaches js_typed_feedback_object_set_field_by_name, which has no strict parameter and rejects by throwing unconditionally.

The contrast that proves it: o.x++ is correct, because it lowers to Expr::PropertyUpdate, which does carry ctx.current_strict. Two spellings of the same operation, one with the field and one without.

Why this is worse than it looks

A CommonJS bundle is sloppy code from top to bottom — that is where #9394 surfaced. A spurious TypeError on o.x += 1 against a frozen object stops a program that node runs to completion, so this is a hard failure rather than a wrong value.

Fix shape

Add the strictness field to Expr::PropertySet and thread it to the runtime entry, mirroring what Expr::PropertyUpdate already does and what #9426 did for Expr::IndexSet (assignment_strict: bool, rendered as (I32, strict_flag)).

Expr::PropertySet has 181 construction sites, which is why it was not folded into PR #9458 — it is its own change, and most of those sites are one-line updates that want a careful default rather than a blanket false.

The runtime side needs a sloppy twin for the rejection path, the same split #9426 made for array element stores (js_array_set_f64_extend_sloppy alongside the strict entry).

Verification bar

A .cts fixture (CommonJS in both runtimes — this repo is "type": "module", so a .ts is strict ESM and cannot express the sloppy arm) byte-compared to node --experimental-strip-types, demonstrated failing on unfixed origin/main. Assert both arms: every shape above in sloppy code where it must be silent, and in "use strict" code where it must throw. Asserting only one arm is exactly what let #9326 and #9394 through.

Cover +=, -=, *=, &&=, ||=, ??=, for (o.x of …), destructuring [o.x] = arr and ({a: o.x} = obj), on a frozen object, a sealed object, a non-writable own property, an inherited non-writable property, and a getter-only accessor.

Found while fixing #9422/#9423.

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