Skip to content

Media: Scope the crossorigin bookmark to its media element - #13221

Open
itzmekhokan wants to merge 2 commits into
WordPress:trunkfrom
itzmekhokan:fix/65930-crossorigin-source-scope
Open

Media: Scope the crossorigin bookmark to its media element#13221
itzmekhokan wants to merge 2 commits into
WordPress:trunkfrom
itzmekhokan:fix/65930-crossorigin-source-scope

Conversation

@itzmekhokan

@itzmekhokan itzmekhokan commented Aug 21, 2026

Copy link
Copy Markdown

A <source> element could hand crossorigin="anonymous" to the wrong media element, or raise _doing_it_wrong() from inside the cross-origin isolation output buffer.

wp_add_crossorigin_attributes() tracks the AUDIO or VIDEO parent of a SOURCE with an audio-video-parent bookmark that is never scoped to the element it was set on, and releases it after the first cross-origin SOURCE. A second SOURCE then seeks a bookmark that no longer exists, and a SOURCE outside any media element walks back to an unrelated one. This visits closing tags so the bookmark cannot outlive its element, keeps it across sibling sources, and only seeks a bookmark the loop set.

The accepted set only narrows, and the loop now performs fewer seek() calls than before.

Trac ticket: https://core.trac.wordpress.org/ticket/65930

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5
Used for: diagnosing the bookmark scoping fault, implementing the fix, and extending the existing data providers. All changes were reviewed and validated by me.

`wp_add_crossorigin_attributes()` tracked the AUDIO or VIDEO element a
SOURCE belongs to with a bookmark that was never scoped to the element
it was set on, and released it after the first cross-origin SOURCE. A
second SOURCE then sought a bookmark that no longer existed, raising
`_doing_it_wrong()` from inside the output buffer's display handler,
while a SOURCE outside any media element walked back to an unrelated
one and marked that instead.

Visit closing tags so the bookmark cannot outlive its element, keep it
across sibling SOURCE elements, and only seek a bookmark this loop set.

Fixes #65930.
@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props khokansardar, adamsilverstein.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@adamsilverstein

Copy link
Copy Markdown
Member

@itzmekhokan - were you able to reproduce the bug? Are the two examples in your test enough to cause the whitescreen if present in content?

Comment thread src/wp-includes/media.php
`WP_HTML_Tag_Processor::seek()` returns false without moving the cursor once
`MAX_SEEK_OPS` is exceeded, so marking the parent without checking the return
value puts `crossorigin` on the SOURCE, where it does nothing for CORS, and
leaves the media element unmarked.

The bookmark is dropped whether or not the seek lands, since the budget never
comes back and every remaining SOURCE of that element would only raise the
same notice again. A media element marked from its own attribute drops it as
well, rather than having its SOURCE children seek back to re-set an attribute
that is already there.

See #65930.
@itzmekhokan

Copy link
Copy Markdown
Author

Yes, reproduced — guard pushed as 7456c6a.

Are the two examples enough? No. The _doing_it_wrong() only becomes a white screen if an error handler is installed that itself buffers or throws. Same markup, trunk, WP_DEBUG on:

error handler result
none notice discarded, page renders fine
calls ob_start() Fatal error: ob_start(): Cannot use output buffering in output buffering display handlers
throws ErrorException Fatal error: Uncaught ErrorException: …seek was called incorrectly. Unknown bookmark name.

Which is probably why @b0b3k couldn't bisect it — the plugin installing the handler isn't the one printing the <source>.

Your seek finding: holds. 501 videos, trunk marks 0 sources, my PR marked 1. Fixed, with one tweak — $media_bookmark = null needs to stay outside the if. seek_count never resets, so once the budget is gone every remaining <source> just retries: 16 notices instead of 6. Also dropped the bookmark when a media element is marked from its own src, where it was seeking back to re-set an attribute already there.

Your test is in as written, green single + multisite. Diffed trunk against the patch over 110 markup shapes: 99 identical, 11 differ and all 11 are this fix. One behavior note — <video><audio src="/local.mp3"></audio><source src="cdn"></video> marks the audio on trunk and marks nothing now; a trailing <source> is ignored by the browser anyway, so unmarked beats wrong element.

Agree the >500 case is a separate follow-up. It's removable if it's worth it — a lookahead pass instead of seeking back, no bookmarks or seek() in the buffer at all.

@adamsilverstein

Copy link
Copy Markdown
Member

Are the two examples enough? No. The _doing_it_wrong() only becomes a white screen if an error handler is installed that itself buffers or throws.

That lowers the severity of the issue significantly although it still seems worth fixing.

@adamsilverstein

Copy link
Copy Markdown
Member

cc: @dmsnell for feedback on the HTML Processor code, double checking this isn't an API issue.

@adamsilverstein adamsilverstein left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks good to me and the tests are helpful. I'm researching whether we still need this code and if there is a better solution here.

@adamsilverstein

adamsilverstein commented Sep 8, 2026

Copy link
Copy Markdown
Member

Reviewing this again, I hjave a suspicion we don't need this crossorigin="anonymous" code at all. This looks like a remnant from our previous use of COOP/COEP headers, before we switched to DIP. So I think we can remove it from Gutenberg and Core (and add it to the client-side-media-everywhere plugin that enables the feature for Safari where this would still be needed).

I'm going to open some separate PRs proposing that and if they test well we can eliminate this entire class of errors.

cc: @swissspidy

@adamsilverstein

Copy link
Copy Markdown
Member

@itzmekhokan I think we can remove this processing entirely, can you check if this PR resolves the issue? If so, please close out this PR in favor of that one:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants