Skip to content

Conversation

@mrobinson
Copy link
Member

@mrobinson mrobinson commented Nov 18, 2025

Many WebGL objects refer to a WebGLRenderingContext and rely on it for
messaging to the WebGLThread. This poses a problem, because WebGL
objects often need to send a message to the WebGLThread during their
Drop implementation. If the Drop is triggered as part of garbage
collection, references to the WebGLRenderingContext might be invalid,
if they were garbage collected first as part of the same harvest.

This change makes it so that all of these objects store a WeakRef
instead of a Dom<>. The WeakRef is only used if it can be rooted,
otherwise a ContextLost error is given. In cases where only messaging
is needed, a cloned WebGLMsgSender is used to perform messages
regardless of whether the context is garbage collected or not.

This isn't a replacement for #37622, but should make it easier to implement as
the WebGLMsgSender and the WeakRef could be stored in the droppable
portion of the DOM object.

Testing: This fixes a use-after-free issue which is mainly detectable via ASAN
builds. Since we do not run ASAN on CI, this is a bit hard to create automated
tests for. I verified that this fixed the issue manually.
Fixes: #40655.

@mrobinson mrobinson requested a review from gterzian as a code owner November 18, 2025 18:20
@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label Nov 18, 2025
@mrobinson mrobinson added the T-linux-wpt Do a try run of the WPT label Nov 18, 2025
@github-actions github-actions bot removed the T-linux-wpt Do a try run of the WPT label Nov 18, 2025
@github-actions
Copy link

🔨 Triggering try run (#19477498670) for Linux (WPT)

@github-actions
Copy link

Test results for linux-wpt from try job (#19477498670):

Flaky unexpected result (53)
  • OK /IndexedDB/idbfactory_open.any.html
    • FAIL [expected PASS] subtest: Calling open() with version argument 1.5 should not throw.

      assert_equals: version expected 1 but got 9007199254740991
      

  • PASS [expected FAIL] /_mozilla/css/linear_gradients_reverse_a.html
  • CRASH [expected PASS] /_mozilla/shadow-dom/move-element-with-ua-shadow-tree-crash.html (#39473)
  • OK /_mozilla/webxr/create_session.https.html
    • FAIL [expected PASS] subtest: create_session

      can't access property "simulateDeviceConnection", navigator.xr.test is undefined
      

  • OK /_mozilla/webxr/obtain_frame.https.html
    • FAIL [expected PASS] subtest: obtain_frame

      promise_test: Unhandled rejection with value: object "TypeError: can't access property "simulateDeviceConnection", navigator.xr.test is undefined"
      

  • ERROR [expected TIMEOUT] /_mozilla/webxr/sessionavailable.https.html
  • CRASH [expected OK] /_webgl/conformance/glsl/bugs/conditional-discard-optimization.html
  • CRASH [expected OK] /_webgl/conformance/glsl/bugs/floored-division-accuracy.html
  • CRASH [expected OK] /_webgl/conformance/ogles/GL/mat/mat_017_to_024.html
  • CRASH [expected OK] /_webgl/conformance/ogles/GL/pow/pow_009_to_016.html
  • CRASH [expected OK] /_webgl/conformance/ogles/GL/sin/sin_001_to_006.html (#21493)
  • CRASH [expected OK] /_webgl/conformance/renderbuffers/renderbuffer-initialization.html
  • CRASH [expected OK] /_webgl/conformance/rendering/clipping-wide-points.html
  • CRASH [expected OK] /_webgl/conformance/rendering/framebuffer-texture-switch.html
  • CRASH [expected OK] /_webgl/conformance/state/fb-attach-implicit-target-assignment.html
  • CRASH [expected OK] /_webgl/conformance2/extensions/ext-texture-filter-anisotropic.html
  • TIMEOUT /content-security-policy/inheritance/location-reload.html (#38983)
    • PASS [expected FAIL] subtest: location.reload() of empty iframe.
  • OK [expected TIMEOUT] /cookiestore/httponly_cookies.https.window.html (#40697)
    • FAIL [expected TIMEOUT] subtest: HttpOnly cookies are not observed

      assert_equals: HttpOnly cookie we wrote using HTTP in cookie jar is invisible to script expected (undefined) undefined but got (string) "cspViolationReportCookie1=1"
      

    • FAIL [expected NOTRUN] subtest: HttpOnly cookies can not be set by document.cookie

      assert_equals: Trying to store an HttpOnly cookie with document.cookie fails expected "cookie1=value1; cookie3=value3" but got "HTTPONLY-cookie=value; cookie1=value1; cookie3=value3"
      

    • FAIL [expected NOTRUN] subtest: HttpOnly cookies can not be set by CookieStore

      assert_equals: httpOnly is not an option for CookieStore.set() expected "cookie1=value1; cookie2=value2; cookie3=value3" but got "HTTPONLY-cookie=value; cookie1=value1; cookie2=value2; cookie3=value3"
      

    • FAIL [expected NOTRUN] subtest: HttpOnly cookies are not deleted/overwritten

      assert_equals: HttpOnly cookie is not deleted expected (string) "HTTPONLY-cookie=value" but got (undefined) undefined
      

  • CRASH [expected PASS] /css/CSS2/positioning/position-relative-004.xht
  • FAIL [expected PASS] /css/css-backgrounds/background-size-041.html
  • OK /css/css-cascade/layer-cssom-order-reverse.html (#36094)
    • PASS [expected FAIL] subtest: Delete layer invalidates @font-face
  • OK /css/css-cascade/layer-font-face-override.html (#35935)
    • PASS [expected FAIL] subtest: @font-face override update with appended sheet 1
    • PASS [expected FAIL] subtest: @font-face override update with appended sheet 2
  • OK /custom-elements/form-associated/ElementInternals-setFormValue.html (#29174)
    • PASS [expected FAIL] subtest: Null value should submit nothing
  • CRASH [expected ERROR] /dom/abort/AbortSignal.https.any.shadowrealm-in-audioworklet.html (#38763)
  • OK /fetch/fetch-later/send-on-discard/not-send-after-abort.https.window.html (#40696)
    • FAIL [expected PASS] subtest: A discarded document does not send an already aborted fetchLater request.

      assert_equals: Number of sent beacons does not match expected count: expected 1 but got 0
      

  • OK [expected CRASH] /fetch/metadata/generated/element-iframe.https.sub.html (#40341)
  • ERROR /fetch/metadata/generated/serviceworker.https.sub.html (#36247)
    • FAIL [expected PASS] subtest: sec-fetch-site - Same origin, no options - registration

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

  • OK /html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin.window.html (#29049)
    • PASS [expected FAIL] subtest: Same-origin navigation started from unload handler must be ignored
  • OK /html/browsers/browsing-the-web/navigating-across-documents/refresh/same-document-refresh.html (#34597)
    • FAIL [expected PASS] subtest: Same-Document Referrer from Refresh

      assert_equals: original page loads expected "http://web-platform.test:8000/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refresh-with-section.sub.html?url=%23section" but got "http://web-platform.test:8000/html/browsers/browsing-the-web/navigating-across-documents/refresh/resources/refresh-with-section.sub.html?url=%23section#section"
      

  • OK /html/browsers/history/the-history-interface/traverse_the_history_5.html (#21383)
    • PASS [expected FAIL] subtest: Multiple history traversals, last would be aborted
  • TIMEOUT [expected OK] /html/interaction/focus/the-autofocus-attribute/document-with-fragment-empty.html (#28259)
    • TIMEOUT [expected FAIL] subtest: Autofocus elements in top-level browsing context's documents with empty fragments should work.

      Test timed out
      

  • TIMEOUT [expected OK] /html/interaction/focus/the-autofocus-attribute/update-the-rendering.html (#24145)
    • TIMEOUT [expected FAIL] subtest: "Flush autofocus candidates" should be happen before a scroll event and animation frame callbacks

      Test timed out
      

  • OK /html/semantics/forms/form-submission-0/multipart-formdata.window.html (#28725)
    • FAIL [expected PASS] subtest: multipart/form-data: Basic test (formdata event)

      assert_equals: expected "\r\nContent-Disposition: form-data; name=\"basic\"\r\n\r\ntest\r\n--\r\n" but got ""
      

  • OK /html/semantics/forms/form-submission-0/text-plain.window.html (#28687)
    • FAIL [expected PASS] subtest: text/plain: Basic File test (normal form)

      assert_equals: expected "basic=file-test.txt\r\n" but got ""
      

    • PASS [expected FAIL] subtest: text/plain: 0x00 in name (formdata event)
    • PASS [expected FAIL] subtest: text/plain: single quote in value (formdata event)
  • OK [expected CRASH] /html/semantics/forms/the-fieldset-element/disabled-003.html (#31730, #39631)
  • OK [expected ERROR] /html/user-activation/no-activation-thru-escape-key.html (#40343)
  • CRASH [expected OK] /mimesniff/sniffing/html.window.html (#40471)
  • OK /navigation-timing/test-navigation-type-reload.html (#33334)
    • PASS [expected FAIL] subtest: Reload domComplete &gt; Original domComplete
    • PASS [expected FAIL] subtest: Reload domContentLoadedEventEnd &gt; Original domContentLoadedEventEnd
    • PASS [expected FAIL] subtest: Reload domContentLoadedEventStart &gt; Original domContentLoadedEventStart
    • PASS [expected FAIL] subtest: Reload domInteractive &gt; Original domInteractive
    • PASS [expected FAIL] subtest: Reload fetchStart &gt; Original fetchStart
    • PASS [expected FAIL] subtest: Reload loadEventEnd &gt; Original loadEventEnd
    • PASS [expected FAIL] subtest: Reload loadEventStart &gt; Original loadEventStart
  • CRASH [expected OK] /pointerevents/pointerevent_multiple_pointerover_no_pointer_movement.html
  • OK /preload/prefetch-document.html (#37210)
    • FAIL [expected PASS] subtest: different-site document prefetch with 'as=document' should not be consumed

      assert_equals: expected 2 but got 1
      

  • OK /preload/preload-invalid-resources.html (#39091)
    • PASS [expected FAIL] subtest: Preloading an invalid image (invalid data) should preload and not re-fetch
  • ERROR [expected OK] /resource-timing/cors-preflight.any.html (#28694)
  • TIMEOUT /resource-timing/test_resource_timing.https.html (#25216)
    • PASS [expected FAIL] subtest: PerformanceEntry has correct name, initiatorType, startTime, and duration (img)
  • OK /service-workers/service-worker/fetch-event.https.html (#36234)
    • PASS [expected FAIL] subtest: Service Worker falls back to network in fetch event with POST form
  • CRASH [expected ERROR] /trusted-types/SharedWorker-setTimeout-setInterval.html
  • CRASH [expected OK] /trusted-types/eval-csp-no-tt.html
  • OK [expected TIMEOUT] /trusted-types/trusted-types-navigation.html?31-35 (#38034)
    • PASS [expected TIMEOUT] subtest: Navigate a frame via form-submission with javascript:-urls w/ default policy in report-only mode.
    • FAIL [expected NOTRUN] subtest: Navigate a window via form-submission with javascript:-urls w/ a default policy throwing an exception in enforcing mode.

      promise_test: Unhandled rejection with value: "Unexpected message received: \"No securitypolicyviolation reported!\""
      

    • FAIL [expected NOTRUN] subtest: Navigate a window via form-submission with javascript:-urls w/ a default policy throwing an exception in report-only mode.

      promise_test: Unhandled rejection with value: "Unexpected message received: \"No securitypolicyviolation reported!\""
      

    • FAIL [expected NOTRUN] subtest: Navigate a window via form-submission with javascript:-urls w/ a default policy making the URL invalid in enforcing mode.

      promise_test: Unhandled rejection with value: "Unexpected message received: \"No securitypolicyviolation reported!\""
      

  • CRASH [expected OK] /uievents/click/click_event_target_siblings.html
  • CRASH [expected OK] /wasm/webapi/esm-integration/wasm-to-wasm-link-error.tentative.html
  • CRASH [expected OK] /wasm/webapi/esm-integration/worklet-import-source-phase.tentative.https.html
  • OK /webxr/xrSession_features_deviceSupport.https.html (#24357)
    • FAIL [expected PASS] subtest: Immersive XRSession requests with no supported device should reject

      assert_unreached: Should have rejected: undefined Reached unreachable code
      

  • CRASH [expected ERROR] /workers/Worker-constructor-proto.any.serviceworker.html
  • ERROR [expected OK] /workers/baseurl/alpha/import-in-moduleworker.html (#21315)
Stable unexpected results that are known to be intermittent (22)
  • TIMEOUT /FileAPI/url/url-in-tags-revoke.window.html (#19978)
    • TIMEOUT [expected PASS] subtest: Fetching a blob URL immediately before revoking it works in &lt;script&gt; tags.

      Test timed out
      

  • OK /IndexedDB/idbobjectstore_getAll.any.html (#39276)
    • PASS [expected FAIL] subtest: Get all values with transaction.commit()
  • OK /IndexedDB/idbobjectstore_getAll.any.worker.html (#39400)
    • PASS [expected FAIL] subtest: Get all values with transaction.commit()
  • FAIL [expected PASS] /_mozilla/mozilla/sslfail.html (#10760)
  • TIMEOUT [expected OK] /_mozilla/mozilla/window_resize_event.html (#36741)
    • TIMEOUT [expected PASS] subtest: Popup onresize event fires after resizeTo

      Test timed out
      

  • OK /css/css-fonts/generic-family-keywords-001.html (#37467)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(fangsong)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(kai)
  • OK /css/css-fonts/generic-family-keywords-003.html (#38994)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(kai) (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted generic(khmer-mul) (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted ui-monospace (drawing text in a canvas)
    • PASS [expected FAIL] subtest: @font-face matching for quoted and unquoted ui-rounded (drawing text in a canvas)
  • TIMEOUT [expected OK] /fetch/api/redirect/redirect-keepalive.https.any.html (#32153)
    • TIMEOUT [expected PASS] subtest: [keepalive][iframe][load] mixed content redirect; setting up

      Test timed out
      

  • OK /fetch/fetch-later/permissions-policy/deferred-fetch-allowed-by-permissions-policy.https.window.html (#40478)
    • FAIL [expected PASS] subtest: Permissions policy header: "deferred-fetch=*" allows fetchLater() in the top-level document.

      assert_equals: Number of sent beacons does not match expected count: expected 1 but got 0
      

  • OK /fetch/metadata/generated/css-font-face.sub.tentative.html (#34624)
    • PASS [expected FAIL] subtest: sec-fetch-storage-access - Not sent to non-trustworthy same-site destination
    • FAIL [expected PASS] subtest: sec-fetch-storage-access - Not sent to non-trustworthy cross-site destination

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

  • OK /fetch/metadata/generated/element-img-environment-change.https.sub.html (#30111)
    • PASS [expected FAIL] subtest: sec-fetch-site - Cross-site, no attributes
    • FAIL [expected PASS] subtest: sec-fetch-site - Same-Origin -&gt; Same-Site -&gt; Same-Origin redirect, no attributes

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

    • PASS [expected FAIL] subtest: sec-fetch-site - Cross-Site -&gt; Same Origin, no attributes
    • FAIL [expected PASS] subtest: sec-fetch-site - Cross-Site -&gt; Same-Site, no attributes

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

    • PASS [expected FAIL] subtest: sec-fetch-site - Cross-Site -&gt; Cross-Site, no attributes
    • FAIL [expected PASS] subtest: sec-fetch-site - Same-Origin -&gt; Same Origin, no attributes

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

    • FAIL [expected PASS] subtest: sec-fetch-mode - attributes: crossorigin

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

    • FAIL [expected PASS] subtest: sec-fetch-mode - attributes: crossorigin=use-credentials

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

    • PASS [expected FAIL] subtest: sec-fetch-dest - no attributes
  • OK /fetch/metadata/generated/element-img-environment-change.sub.html (#30111)
    • PASS [expected FAIL] subtest: sec-fetch-site - Not sent to non-trustworthy cross-site destination, no attributes
    • PASS [expected FAIL] subtest: sec-fetch-mode - Not sent to non-trustworthy same-origin destination, no attributes
    • PASS [expected FAIL] subtest: sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no attributes
    • FAIL [expected PASS] subtest: sec-fetch-user - Not sent to non-trustworthy same-origin destination, no attributes

      promise_test: Unhandled rejection with value: object "Error: Failed to query for recorded headers."
      

    • PASS [expected FAIL] subtest: sec-fetch-user - Not sent to non-trustworthy cross-site destination, no attributes
    • PASS [expected FAIL] subtest: sec-fetch-storage-access - Not sent to non-trustworthy same-origin destination, no attributes
  • OK /html/browsers/browsing-the-web/navigating-across-documents/005.html (#27062)
    • FAIL [expected PASS] subtest: Link with onclick navigation and href navigation

      assert_equals: expected "href" but got "click"
      

  • OK /html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-cross-origin.sub.window.html (#29056)
    • FAIL [expected PASS] subtest: Cross-origin navigation started from unload handler must be ignored

      promise_test: Unhandled rejection with value: object "SecurityError: The operation is insecure."
      

  • OK /html/browsers/history/the-history-interface/traverse_the_history_3.html (#21383)
    • PASS [expected FAIL] subtest: Multiple history traversals, last would be aborted
  • ERROR [expected OK] /html/infrastructure/common-dom-interfaces/collections/domstringlist.html (#40665)
  • OK [expected TIMEOUT] /html/interaction/focus/the-autofocus-attribute/autofocus-dialog.html (#29087)
    • FAIL [expected TIMEOUT] subtest: &lt;dialog&gt;-contained autofocus element gets focused when the dialog is shown

      assert_equals: expected "DIV" but got "BODY"
      

  • TIMEOUT [expected ERROR] /html/semantics/links/links-created-by-a-and-area-elements/target_blank_implicit_noopener_base.html (#40347)
  • CRASH [expected OK] /pointerevents/compat/pointerevent_touch-action_two-finger_interaction.html (#40418)
  • OK /preload/preload-error.sub.html (#37177)
    • FAIL [expected PASS] subtest: success (fetch): main

      assert_greater_than: http://web-platform.test:8000/preload/resources/dummy.xml?label=fetch should be loaded expected a number greater than 0 but got 0
      

  • OK /reporting/same-origin-same-site-credentials.https.sub.html (#40479)
    • FAIL [expected PASS] subtest: Reporting endpoints received credentials.

      assert_equals: No additional cookies were received expected 4 but got 5
      

  • TIMEOUT [expected OK] /trusted-types/trusted-types-navigation.html?26-30 (#38807)
    • TIMEOUT [expected FAIL] subtest: Navigate a window via form-submission with javascript:-urls in report-only mode.

      Test timed out
      

    • NOTRUN [expected PASS] subtest: Navigate a window via form-submission with javascript:-urls w/ default policy in report-only mode.
    • NOTRUN [expected FAIL] subtest: Navigate a frame via form-submission with javascript:-urls in enforcing mode.
    • NOTRUN [expected PASS] subtest: Navigate a frame via form-submission with javascript:-urls w/ default policy in enforcing mode.

@github-actions
Copy link

✨ Try run (#19477498670) succeeded.

if !std::ptr::eq(program.context(), &*self.base) {
self.base.webgl_error(InvalidOperation);
if let Err(error) = self.base.validate_ownership(program) {
self.base.webgl_error(error);
Copy link
Member

@mukilan mukilan Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it correct that this sets the last_error to a ContextLost error in the case that the program's owning context is lost? Presumably that will only happen if the self context is not the owner, as the current method means the self context is still alive, so shouldn't it still return an INVALID_OPERATION error?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I have modified validate_ownership to return Err(InvalidOperation) in this case.

}

let context = self.upcast::<WebGLObject>().context();
let Some(context) = self.upcast::<WebGLObject>().context() else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seem like in many places we can remove the ::<WebGLObject> annotation as WebGLObject is the only ancestor interface and the type checker is able to infer that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed all type annotations on these type of upcasts.

@servo-highfive servo-highfive removed the S-awaiting-review There is new code that needs to be reviewed. label Nov 19, 2025
Many WebGL objects refer to a `WebGLRenderingContext` and rely on it for
messaging to the `WebGLThread`. This poses a problem, because WebGL
objects often need to send a message to the `WebGLThread` during their
`Drop` implementation. If the `Drop` is triggered as part of garbage
collection, references to the `WebGLRenderingContext` might be invalid,
if they were garbage collected first as part of the same harvest.

This changes makes it so that all of these objects store a `WeakRef`
instead of a `Dom<>`. The `WeakRef` is only used if it can be rooted,
otherwise a `ContextLost` error is given. In cases where only messaging
is needed a cloned `WebGLMsgSender` is used to perform messages
regardless of whether the context is garbage collected or not.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
@servo-highfive servo-highfive added the S-awaiting-review There is new code that needs to be reviewed. label Nov 19, 2025
@mrobinson mrobinson enabled auto-merge November 19, 2025 12:24
@mrobinson mrobinson added this pull request to the merge queue Nov 19, 2025
@servo-highfive servo-highfive added the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label Nov 19, 2025
Merged via the queue into servo:main with commit 1a99f74 Nov 19, 2025
37 checks passed
@mrobinson mrobinson deleted the webgl-context branch November 19, 2025 13:01
@servo-highfive servo-highfive removed the S-awaiting-merge The PR is in the process of compiling and running tests on the automated CI. label Nov 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-awaiting-review There is new code that needs to be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ASAN: heap-use-after-free in canvas_traits::webgl::WebGLMsgSender::send: on jingdong.com

4 participants