Closed Bug 2077725 Opened 1 month ago Closed 1 month ago

Ion skips proxy-get validation for immutable typed-array elements

Categories

(Core :: JavaScript Engine, defect, P3)

defect

Tracking

()

RESOLVED FIXED
160 Branch
Tracking Status
firefox-esr115 --- unaffected
firefox-esr153 --- disabled
firefox157 --- disabled
firefox158 --- disabled
firefox159 --- wontfix
firefox160 --- fixed

People

(Reporter: bugmon, Assigned: iain)

References

(Blocks 1 open bug)

Details

(Keywords: testcase)

Attachments

(5 files, 2 external files)

NewTypedArrayObject initializes immutable typed-array shapes without NeedsProxyGetSetResultValidation, although their indexed properties are non-configurable and non-writable. Their class also has no resolve hook. Consequently, MacroAssembler::branchTestObjectNeedsProxyResultValidation lets Ion omit descriptor validation and accept a proxy-get result that should cause a TypeError.

A web page can make the same JavaScript operation behave differently after JIT compilation by accessing an immutable typed array through a scripted proxy. This requires Nightly’s disabled-by-default javascript.options.experimental.arraybuffer_immutable preference. The demonstrated impact is a missing required exception; the evidence does not establish memory corruption or a process-boundary escape.

This finding was discovered during variant analysis seeded by bug 1536768.

Build Info

Affected Code

File: js/src/vm/TypedArrayObject.cpp, line 388-391

  Rooted<SharedShape*> shape(
      cx,
      SharedShape::getInitialShape(cx, clasp, cx->realm(), AsTaggedProto(proto),
                                   nfixed, ObjectFlags()));

File: js/src/vm/Shape-inl.h, line 94-99

  if (prop.isTypedArrayElement()) {
    if (obj->is<ImmutableTypedArrayObject>()) {
      return {JS::PropertyAttribute::Enumerable};
    }
    return {JS::PropertyAttribute::Configurable,
            JS::PropertyAttribute::Enumerable, JS::PropertyAttribute::Writable};

File: js/src/jit/MacroAssembler.cpp, line 6218-6235

  loadPtr(Address(obj, JSObject::offsetOfShape()), scratch);
  branchTest32(Assembler::Zero,
               Address(scratch, Shape::offsetOfImmutableFlags()),
               Imm32(Shape::isNativeBit()), doValidation);
  static_assert(sizeof(ObjectFlags) == sizeof(uint32_t));
  load32(Address(scratch, Shape::offsetOfObjectFlags()), scratch);
  branchTest32(Assembler::NonZero, scratch,
               Imm32(uint32_t(ObjectFlag::NeedsProxyGetSetResultValidation)),
               doValidation);

  loadPtr(Address(obj, JSObject::offsetOfShape()), scratch);
  loadPtr(Address(scratch, Shape::offsetOfBaseShape()), scratch);
  loadPtr(Address(scratch, BaseShape::offsetOfClasp()), scratch);
  loadPtr(Address(scratch, offsetof(JSClass, cOps)), scratch);
  branchTestPtr(Assembler::Zero, scratch, scratch, skipValidation);
  loadPtr(Address(scratch, offsetof(JSClassOps, resolve)), scratch);
  branchTestPtr(Assembler::NonZero, scratch, scratch, doValidation);
  bind(&done);

File: js/src/jit/CodeGenerator.cpp, line 18012-18020

  using Fn = bool (*)(JSContext*, HandleObject, HandleValue, HandleValue,
                      MutableHandleValue);
  OutOfLineCode* ool = oolCallVM<Fn, CheckProxyGetByValueResult>(
      ins, ArgList(scratch, id, value), StoreValueTo(value));

  masm.unboxObject(target, scratch);
  masm.branchTestObjectNeedsProxyResultValidation(Assembler::NonZero, scratch,
                                                  scratch2, ool->entry());
  masm.bind(ool->rejoin());

At revision f384c89fc98f8cf5e6ffa0caf568868b29fba90a, GetPropIRGenerator::tryAttachScriptedProxy guards the target only as a native object (js/src/jit/CacheIR.cpp:1746). ImmutableTypedArrayClassOps has no resolve hook (js/src/vm/TypedArrayObject.cpp:6311), so the missing shape flag makes the shared predicate skip validation. The ordinary validator checks the actual descriptor and rejects an unequal protected-property value (js/src/proxy/ScriptedProxyHandler.cpp:1194). The browser feature is Nightly-only and disabled by default (modules/libpref/init/StaticPrefList.yaml:9864).

Exploit Chain

  1. Run JavaScript with immutable ArrayBuffers enabled.
  2. Warm an indexed getter using a scripted proxy whose target is an ordinary object and whose get trap returns 71.
  3. Pass the same getter a proxy over a Uint8Array backed by an immutable, zero-initialized ArrayBuffer.
  4. Ion’s native-object validation predicate skips the descriptor check, so the operation completes instead of throwing the TypeError produced by the interpreter.

Steps to Reproduce

  1. Use the unmodified Firefox source at revision f384c89fc98f8cf5e6ffa0caf568868b29fba90a. Build with build_firefox using /app/mozconfigs/mozconfig.js.linux.debug.
  2. Pass the exact contents of /tmp/tmph_ombtrt/proxy-immutable.js to js_shell_differential_evaluator. Set both shell paths to /firefox/obj-js-debug/dist/bin/js and timeout to 60 seconds.
  3. Use flags_a=["--enable-arraybuffer-immutable","--no-jit-backend","--no-threads"] and flags_b=["--enable-arraybuffer-immutable","--ion-eager","--no-threads"]. The evaluator adds --fuzzing-safe. Observe exit codes 0/3 and differs: true.
  4. Repeat with --differential-testing added to both flag sets. Observe exit codes 0/3 and differs: true again.
  5. For corroboration, build with /app/mozconfigs/mozconfig.linux.asan.fuzzing and repeat the normal comparison using /firefox/obj-firefox-asan/dist/bin/js. Observe the same 0/3 divergence.
  6. Replacing Ion with --no-ion --baseline-eager, adding --cache-ir-stubs=off, or using /tmp/tmph_ombtrt/proxy-immutable-control.js produces exit codes 0/0. No source patches, browser preference changes, or external services are required for the shell reproduction.

Security Impact

  • Severity: Low
  • Attacker capability: Cause an optimized JavaScript proxy access to omit a required exception. No memory corruption, sensitive-data disclosure, or security-boundary crossing was demonstrated.
  • Preconditions: Immutable ArrayBuffers must be enabled, and the scripted-proxy getter must reach the affected Ion path. Browser exposure requires Nightly’s disabled-by-default javascript.options.experimental.arraybuffer_immutable preference.

ASAN Report

No sanitizer crash occurred; this is a js_differential finding.

Independent debug run:
argv_a: /firefox/obj-js-debug/dist/bin/js --fuzzing-safe --enable-arraybuffer-immutable --no-jit-backend --no-threads /tmp/fx_audit_jsdiff_46texbqc/testcase.js
argv_b: /firefox/obj-js-debug/dist/bin/js --fuzzing-safe --enable-arraybuffer-immutable --ion-eager --no-threads /tmp/fx_audit_jsdiff_46texbqc/testcase.js
exit_codes: 0/3
differs: true
stderr_b:
/tmp/fx_audit_jsdiff_46texbqc/testcase.js:11:19 Error: missing TypeError for immutable element
Stack:
  @/tmp/fx_audit_jsdiff_46texbqc/testcase.js:11:19

Independent differential-testing run:
argv_a: /firefox/obj-js-debug/dist/bin/js --fuzzing-safe --enable-arraybuffer-immutable --no-jit-backend --no-threads --differential-testing /tmp/fx_audit_jsdiff_9lborgjh/testcase.js
argv_b: /firefox/obj-js-debug/dist/bin/js --fuzzing-safe --enable-arraybuffer-immutable --ion-eager --no-threads --differential-testing /tmp/fx_audit_jsdiff_9lborgjh/testcase.js
exit_codes: 0/3
differs: true
stderr_b:
/tmp/fx_audit_jsdiff_9lborgjh/testcase.js:11:19 Error: missing TypeError for immutable element
Stack:
  @/tmp/fx_audit_jsdiff_9lborgjh/testcase.js:11:1

Independent ASAN-build run:
argv_a: /firefox/obj-firefox-asan/dist/bin/js --fuzzing-safe --enable-arraybuffer-immutable --no-jit-backend --no-threads /tmp/fx_audit_jsdiff_28kq0er8/testcase.js
argv_b: /firefox/obj-firefox-asan/dist/bin/js --fuzzing-safe --enable-arraybuffer-immutable --ion-eager --no-threads /tmp/fx_audit_jsdiff_28kq0er8/testcase.js
exit_codes: 0/3
differs: true
stderr_b:
/tmp/fx_audit_jsdiff_28kq0er8/testcase.js:11:19 Error: missing TypeError for immutable element
Stack:
  @/tmp/fx_audit_jsdiff_28kq0er8/testcase.js:11:19

Attached patch fix.patch — — Splinter Review
Attached file crash_stack.txt —
Attached file primary-proof.txt —
Attached file proxy-immutable.js —

This bug was filed automatically by an AI-powered security testing tool. The analysis, testcase, and any suggested patches were generated by an AI model and have NOT been verified by a human.

Please review all details carefully before triaging. The issue was confirmed by an independent verifier, but for non-ASAN issues, the verifier has been made less strict. Additionally, the root cause analysis and suggested fix may contain errors.

Group: core-security → javascript-core-security
See Also: → CVE-2019-9816

Skipping proxy get validation is not exploitable. The patch seems fine, though.

We are required to throw an exception if a Proxy's get trap returns a value that doesn't match the underlying object for a non-writable, non-configurable property. Our proxy fast path depends on the NeedsProxyGetSetResultValidation flag being set on any object that has non-writable, non-configurable properties. Normally this is handled automatically in GetObjectFlagsForNewProperty, but immutable typed array elements don't take that path.

Assignee: nobody → iireland
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true

:iain, please review the release status flags for beta, release, and ESR and update each one as appropriate.

For each version, indicate whether it is affected, unaffected, disabled, or wontfix if it is affected but does not need to be fixed on that branch.

Flags: needinfo?(iireland)
Blocks: sm-security
Severity: -- → S3
Priority: -- → P3

Immutable typed arrays haven't shipped yet, and this is just a corner-case correctness issue. It can ride the trains.

Group: javascript-core-security
Keywords: sec-low
Attachment #9653197 - Attachment description: (secure) → Bug 2077725: Mark immutable TypedArrays for proxy validation r=#spidermonkey-reviewers

Note that when we're deciding whether we have to do this check we also check for a resolve hook, but here we're already looking at a concrete property that should have set the flag when it was added, so having a resolve hook isn't an excuse (and therefore doesn't need to be part of the assertion).

Status: ASSIGNED → RESOLVED
Closed: 1 month ago
Resolution: --- → FIXED
Target Milestone: --- → 160 Branch

Since nightly and release are affected, beta will likely be affected too.
For more information, please visit BugBot documentation.

The patch landed in nightly and beta is affected.

:iain, please make an uplift decision for beta:

  • For beta, nominate the patch for beta uplift approval if the fix should be included in this release, or set status-firefox159 to wontfix if the fix can wait for the next release.

See Requesting an Uplift for documentation on how to request an uplift.

For more information, please visit BugBot documentation.

Flags: needinfo?(iireland)
Regressions: 2080220
Flags: needinfo?(iireland)
QA Whiteboard: [qa-triage-done-c161/b160]
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: