-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Upgrade WebKit to 456cd04b8d5be123bd451f2c007b97dbb3ffed57 #29161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
2650be5
67a210f
4776017
0293909
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -494,31 +494,31 @@ int HTTPParser::onHeadersComplete() | |
| } else { | ||
| auto headers = createHeaders(globalObject); | ||
| RETURN_IF_EXCEPTION(scope, -1); | ||
| args.set(A_HEADERS, headers); | ||
| args.at(A_HEADERS) = headers; | ||
| if (m_parserData.type == HTTP_REQUEST) { | ||
| args.set(A_URL, m_url.toString(globalObject)); | ||
| args.at(A_URL) = m_url.toString(globalObject); | ||
| } | ||
| } | ||
|
|
||
| m_numFields = 0; | ||
| m_numValues = 0; | ||
|
|
||
| if (m_parserData.type == HTTP_REQUEST) { | ||
| args.set(A_METHOD, jsNumber(m_parserData.method)); | ||
| args.at(A_METHOD) = jsNumber(m_parserData.method); | ||
| } | ||
|
|
||
| if (m_parserData.type == HTTP_RESPONSE) { | ||
| args.set(A_STATUS_CODE, jsNumber(m_parserData.status_code)); | ||
| args.set(A_STATUS_MESSAGE, m_statusMessage.toString(globalObject)); | ||
| args.at(A_STATUS_CODE) = jsNumber(m_parserData.status_code); | ||
| args.at(A_STATUS_MESSAGE) = m_statusMessage.toString(globalObject); | ||
| } | ||
|
Comment on lines
+497
to
513
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟣 Pre-existing bug: in NodeHTTPParser.cpp onHeadersComplete(), the calls m_url.toString(globalObject) (HTTP_REQUEST path) and m_statusMessage.toString(globalObject) (HTTP_RESPONSE path) lack RETURN_IF_EXCEPTION(scope, -1) guards. If either call throws on OOM while allocating a JSString, execution continues with a pending VM exception and invokes profiledCall() — violating JSC's core invariant. The fix is to add RETURN_IF_EXCEPTION(scope, -1) after each toString() call; this bug predates the PR but the PR touches these exact lines. Extended reasoning...What the bug is and how it manifests: In NodeHTTPParser.cpp, onHeadersComplete() uses DECLARE_TOP_EXCEPTION_SCOPE. Two calls — m_url.toString(globalObject) at line 499 (HTTP_REQUEST path) and m_statusMessage.toString(globalObject) at line 512 (HTTP_RESPONSE path) — are not followed by RETURN_IF_EXCEPTION. StringPtr::toString() calls JSC::jsString(vm, WTF::String::fromUTF8(...)), which allocates a JSString on the GC heap. If that allocation fails with OutOfMemory, JSC stores a pending exception in the VM and the call returns a garbage JSValue. Without a guard, execution continues: m_numFields/m_numValues are cleared, further args slots are written, and eventually JSC::profiledCall() is invoked with a pending exception still set — which is undefined behavior in JSC and triggers assertions in debug builds. The specific code path that triggers it: Under memory pressure, any GC heap allocation can fail with an OutOfMemoryError. In HTTP_REQUEST mode, args.at(A_URL) = m_url.toString(globalObject) stores a garbage value and sets a pending exception. Execution continues through m_numFields = 0 resets, the A_METHOD/A_VERSION_MAJOR/A_VERSION_MINOR/A_SHOULD_KEEP_ALIVE/A_UPGRADE assignments, and then hits JSC::profiledCall() at line 525 with a pending exception still live. Why existing code does not prevent it: The DECLARE_TOP_EXCEPTION_SCOPE does NOT automatically propagate exceptions — it only asserts in debug builds that no exception is pending at scope boundaries. Protection requires manual RETURN_IF_EXCEPTION calls. Crucially, createHeaders() — called in the same function — uses RETURN_IF_EXCEPTION after every identical toString() call (m_fields[i].toString, m_values[i].toTrimmedString), confirming the codebase treats these as throwable. The omission in onHeadersComplete is an inconsistency, not a deliberate design choice. Addressing the refutation: One verifier argued that jsString() never sets a JS exception and instead causes a hard crash on OOM. However, JSC's GC heap allocator can and does report OOM as a thrown JavaScript OutOfMemoryError — it does not always crash. The createHeaders() function in the same file guards the same toString() pattern with RETURN_IF_EXCEPTION. If jsString() truly could not throw, those guards would be harmless no-ops; their presence reflects the actual JSC semantics. The flush() function at the bottom of the file appends m_url.toString(globalObject) to args without a guard, which is the same bug. Step-by-step proof: (1) HTTP request arrives; onHeadersComplete() is called. (2) m_haveFlushed is false, so createHeaders() succeeds and args.at(A_HEADERS) is set. (3) m_parserData.type == HTTP_REQUEST, so args.at(A_URL) = m_url.toString(globalObject) executes. (4) Under OOM, jsString() fails, sets an OutOfMemoryError on the VM, and returns a garbage JSValue — no guard catches this. (5) Execution falls through: m_numFields = 0; m_numValues = 0; args.at(A_METHOD) = jsNumber(...); additional slots filled. (6) JSC::profiledCall() is invoked with a pending exception. Debug builds ASSERT; release builds have undefined behavior. How to fix it: Add RETURN_IF_EXCEPTION(scope, -1) immediately after args.at(A_URL) = m_url.toString(globalObject) (line 499) and after args.at(A_STATUS_MESSAGE) = m_statusMessage.toString(globalObject) (line 512). The flush() function also appends m_url.toString(globalObject) without a guard and should be fixed in the same pass. |
||
|
|
||
| args.set(A_VERSION_MAJOR, jsNumber(m_parserData.http_major)); | ||
| args.set(A_VERSION_MINOR, jsNumber(m_parserData.http_minor)); | ||
| args.at(A_VERSION_MAJOR) = jsNumber(m_parserData.http_major); | ||
| args.at(A_VERSION_MINOR) = jsNumber(m_parserData.http_minor); | ||
|
|
||
| bool shouldKeepAlive = llhttp_should_keep_alive(&m_parserData); | ||
|
|
||
| args.set(A_SHOULD_KEEP_ALIVE, jsBoolean(shouldKeepAlive)); | ||
| args.set(A_UPGRADE, jsBoolean(m_parserData.upgrade)); | ||
| args.at(A_SHOULD_KEEP_ALIVE) = jsBoolean(shouldKeepAlive); | ||
| args.at(A_UPGRADE) = jsBoolean(m_parserData.upgrade); | ||
|
|
||
| CallData callData = getCallData(onHeadersCompleteCallback); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟣 Pre-existing bug:
WriteBarrierList::moveTo()(WriteBarrierList.h:44-53) callsvalue.clear()on each entry but never callsm_list.clear(), leaving null WriteBarrier slots that accumulate over every watch-mode rebuild. The fix is to addm_list.clear()at the end ofmoveTo().Extended reasoning...
What the bug is:
WriteBarrierList<T>::moveTo()(WriteBarrierList.h:44–53) iteratesm_list, appends non-null cells toarguments, and callsvalue.clear()on each entry — but never removes those entries fromm_listitself. After every call, the vector retains all N slots, now holding nullWriteBarrier<JSPromise>objects.How it manifests in watch mode: Each build cycle,
JSBundlerPlugin__appendDeferPromisecallsdeferredPromises.append(), which pushes newJSPromiseentries to the end ofm_list. AfterJSBundlerPlugin__drainDeferredcallsdeferredPromises.moveTo(), those entries are cleared but not removed. The next cycle appends on top of them. Over N build cycles with P deferred promises per build,m_listgrows to N × P entries — unboundedly. This is an O(N) slow memory leak in long-running watch sessions.Why existing code doesn't prevent it: The
moveTo()implementation simply does not callm_list.clear()(or any shrink/compact operation) after the loop. The only removal path in this class istakeFirst(), which usesremoveAt(0), andremoveFirstMatching(). Neither is called during drain.Secondary breakage —
isEmpty()is always false after the first drain:isEmpty()(line 64) returnsm_list.isEmpty(), which is never true once entries have been appended and cleared. Any caller checkingdeferredPromises.isEmpty()to guard work will get a false positive after the first build cycle.GC overhead: The
visit()traversal (line 55–62) iterates every entry inm_listregardless of whether the WriteBarrier holds a live object. After hundreds of builds, the GC visitor walks hundreds of dead null slots on each collection cycle.Concrete proof — step by step:
defer()→m_list = [WB(P1), WB(P2), WB(P3)](size 3)drainDeferred()→ eachWB.clear()→m_list = [WB(null), WB(null), WB(null)](still size 3)m_list = [null, null, null, WB(P4), WB(P5), WB(P6)](size 6)m_list = [null×6]m_listhas 3N null entriesHow to fix: Add
m_list.clear();at the end of themoveTo()method (optionally followed bym_list.shrinkToFit()or a capacity cap to avoid keeping large allocations around). This is a three-line change in WriteBarrierList.h.Relation to this PR: This PR directly modifies
JSBundlerPlugin__drainDeferred(the sole caller ofdeferredPromises.moveTo()) to update the iterator value type fromEncodedJSValuetoJSValuefollowing the upstream MarkedArgumentBuffer API change. The PR does not introduce or worsen the leak, but it directly touches this code path, making this a good opportunity to fix the underlying issue.