Compare commits

...

28 Commits

Author SHA1 Message Date
trop[bot]
ff343d4808 build(deps): bump dorny/paths-filter from 3.0.2 to 4.0.1 (#51410)
Bumps [dorny/paths-filter](https://github.com/dorny/paths-filter) from 3.0.2 to 4.0.1.
- [Release notes](https://github.com/dorny/paths-filter/releases)
- [Changelog](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
- [Commits](de90cc6fb3...fbd0ab8f3e)

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 15:27:43 -04:00
electron-roller[bot]
5562011d1d chore: bump chromium to 146.0.7680.216 (41-x-y) (#51382)
* chore: bump chromium in DEPS to 146.0.7680.216

* chore: update patches

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: PatchUp <73610968+patchup[bot]@users.noreply.github.com>
2026-04-30 09:37:16 -04:00
Samuel Attard
c9a1cff273 fix: dispatch toast action and reply events from WinRT activation path (#51397)
fix: dispatch toast action and reply events from WinRT activation path (#51286)

* fix: dispatch toast action and reply events from WinRT activation path

ToastEventHandler::Invoke previously returned S_OK without dispatching
whenever the activation arguments looked structured (type=action,
type=reply, or contained &tag=), on the assumption that the COM
INotificationActivationCallback::Activate path would deliver the event
instead. That assumption only holds when Windows actually invokes the
COM activator — which it does for MSIX-packaged apps launched cold, and
for unpackaged apps with a properly-registered CLSID when the app is
not already running. For non-MSIX apps with activationType="foreground"
while the app is running (the common case), Windows raises only the
in-process WinRT Activated event, so action and reply were silently
dropped.

Dispatch structured activations through the same HandleToastActivation
the COM path uses. User input (reply text, selection values) is pulled
from IToastActivatedEventArgs2::UserInput, which carries the data the
COM callback would otherwise have received via
NOTIFICATION_USER_INPUT_DATA.

Also drop the &tag= term from the structured-args check. Plain clicks
in Electron-generated XML don't carry tag=, and a custom toast_xml that
puts tag= on a click argument should now dispatch as a click rather
than being silently dropped.

* fix: release HSTRING out-params from toast activation

Co-authored-by: Shelley Vohr <shelley.vohr@gmail.com>
2026-04-29 23:58:36 -05:00
trop[bot]
8d1475e70b ci: backport secondary siso patch (#51392)
chore: backport secondary siso patch

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: John Kleinschmidt <kleinschmidtorama@gmail.com>
2026-04-29 18:16:21 -05:00
Niklas Wenzel
7e0499d55d feat: support heap profiling in contentTracing (#51178)
* feat: support heap profiling in `contentTracing`

* chore: backport crrev.com/c/7603976 to fix DCHECK failure

* fix: heap profiling test flakes (#51224)
2026-04-29 11:29:46 -07:00
trop[bot]
86b483af5b fix: use no-op header client for Fetch-intercepted requests (#51371)
fix: use no-op header client for Fetch-intercepted requests (#50744)

* fix: use the non-pass-through path for Fetch-intercepted requests

* Revert "fix: use the non-pass-through path for Fetch-intercepted requests"

This reverts commit 395fb8bb8c.

* fix: use no-op header client for Fetch-intercepted requests

* fix: bring back `DCHECK` that was prematurely removed

* style: reformat code

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Noah Gregory <noahmgregory@gmail.com>
2026-04-29 18:42:45 +02:00
trop[bot]
3cd0af44cf fix: include permission element string resources in locale paks (#51373)
The `<geolocation>` HTML element looks up IDS_PERMISSION_REQUEST_GEOLOCATION
via ResourceBundle::GetLocalizedString(). These string IDs are defined in
third_party/blink/public/strings/permission_element_strings.grd.
Electron didn't include that in its pak file, causing CHECK(!data->empty()).

Ths PR adds the per-locale permission_element_strings paks and the
aggregated permission_element_generated_strings pak to electron_paks.gni.
This matches how it's done in `chrome/chrome_repack_locales.gni` and
in `chrome/chrome_paks.gni`.

Xref: https://chromium-review.googlesource.com/c/chromium/src/+/5907626

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2026-04-29 17:57:00 +02:00
electron-roller[bot]
f596e35554 chore: bump chromium to 146.0.7680.208 (41-x-y) (#51089)
* chore: bump chromium in DEPS to 146.0.7680.201

* chore: update patches

* chore: bump chromium in DEPS to 146.0.7680.208

* chore: update patches

* chore: update patches

---------

Co-authored-by: electron-roller[bot] <84116207+electron-roller[bot]@users.noreply.github.com>
Co-authored-by: John Kleinschmidt <kleinschmidtorama@gmail.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2026-04-28 12:38:31 -04:00
trop[bot]
9867bbbde8 docs: add further disposition description (#51369)
* docs: add further disposition description

Co-authored-by: Michaela Laurencin <mlaurencin@electronjs.org>

* add option descriptions

Co-authored-by: Michaela Laurencin <mlaurencin@electronjs.org>

* fix linter

Co-authored-by: Michaela Laurencin <mlaurencin@electronjs.org>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Michaela Laurencin <mlaurencin@electronjs.org>
2026-04-28 11:14:58 -05:00
trop[bot]
6fd63cdba8 fix: prevent crash when calling contentTracing APIs before app is ready (#51352)
* fix: prevent crash when calling contentTracing APIs before app is ready

Added Browser::Get()->is_ready() guards to all contentTracing API functions (startRecording, stopRecording, getCategories, getTraceBufferUsage) so they reject their returned Promises with a clear error message instead of crashing when called before app.whenReady().

Added a crash-case fixture test that validates all four APIs reject properly before readiness and work normally after.

Co-authored-by: om-ghante <mr.omghante1@gmail.com>

* chore: fix linter error in `spec/fixtures/crash-cases/content-tracing-before-ready/` (#51356)

chore: fix linter error in spec/fixtures/crash-cases/content-tracing-before-ready/

introduced earlier today in 6f2e5cd4

* chore: make linter happy

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: om-ghante <mr.omghante1@gmail.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2026-04-28 11:08:11 -05:00
trop[bot]
a629331619 fix: validate header name and value in webRequest.onBeforeSendHeaders (#51365)
* fix: validate header name and value in webRequest.onBeforeSendHeaders

Chromium's net::HttpRequestHeaders::SetHeader() uses CHECK() to enforce
valid header names and values, which causes a fatal crash if the caller
passes invalid strings. When users modify requestHeaders in the
onBeforeSendHeaders callback with invalid header names (e.g. containing
spaces) or invalid header values (e.g. containing CRLF), the
gin::Converter<net::HttpRequestHeaders>::FromV8() calls SetHeader()
directly, triggering the CHECK and crashing the process.

This change adds pre-validation using net::HttpUtil::IsValidHeaderName()
and net::HttpUtil::IsValidHeaderValue() before calling SetHeader(),
silently skipping invalid headers instead of crashing.

Co-authored-by: loufulton <loufulton.cz@gmail.com>

* Update shell/common/gin_converters/net_converter.cc

Co-authored-by: Charles Kerr <charles@charleskerr.com>

Co-authored-by: loufultoncz-coder <loufulton.cz@gmail.com>

* Update spec/api-web-request-spec.ts

Co-authored-by: Charles Kerr <charles@charleskerr.com>

Co-authored-by: loufultoncz-coder <loufulton.cz@gmail.com>

* fix: lint

Co-authored-by: loufulton <loufulton.cz@gmail.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: loufulton <loufulton.cz@gmail.com>
2026-04-28 11:07:36 -05:00
trop[bot]
248ccef775 fix: add MicrotasksScope for worker exit emit in ContextWillDestroy (#51348)
a39108c5a4 (#47244) replaced gin_helper::EmitEvent with a direct
`v8::Function::Call()` in `WebWorkerObserver::ContextWillDestroy`
to avoid re-entering the microtask checkpoint during worker teardown.

V8 `DCHECK()`s that a policy is set. Under the old code path, this
happened with a node::CallbackScope. Under the new code path, it's
possible for a policy to not be set, causing that `DCHECK()` to fail.

This PR copies a39108c5a4's changes in `ShareEnvironmentWithContext()`:
it explicitly adds a `kDoNotRunMicrotasks` scope.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2026-04-27 16:52:26 -07:00
trop[bot]
dd74ff3451 fix: honor webContents.print dpi horizontal/vertical options (#51355)
* fix: honor webContents.print dpi horizontal/vertical options

Co-authored-by: Kunal Dubey <xakep8@protonmail.com>

* style: fix clang-format in print dpi parsing

Co-authored-by: Kunal Dubey <xakep8@protonmail.com>

* style: extract print dpi key constants

Co-authored-by: Kunal Dubey <xakep8@protonmail.com>

* fix: use local dpi constants in print options parser

Co-authored-by: Kunal Dubey <xakep8@protonmail.com>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Kunal Dubey <xakep8@protonmail.com>
2026-04-27 18:36:54 -05:00
trop[bot]
d0afb91da4 fix: make macOS text replacement work on contenteditable (#51343)
* fix: make macOS text replacement work on `contenteditable` (#51289)

* fix: make macOS text replacement work on `contenteditable`

* fix: remove accidentally included patch line

Co-authored-by: Noah Gregory <noahmgregory@gmail.com>

* chore: update patches (trivial only)

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Noah Gregory <noahmgregory@gmail.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2026-04-27 16:57:02 -04:00
Erick Zhao
b88bbb9ca6 docs: mention pre-release installation (#51045) 2026-04-27 11:09:18 -05:00
Charles Kerr
dea65bddd4 fix: crash in AutofillPopup teardown (#51321)
fix: crash in AutofillPopup teardown (#51302)

Fix a crash in AutofillPopupView::Show() when the popup
tried to show itself after the parent's native view had
already gone away during teardown.

2026-04-23T20:44:32.7015810Z Received signal 11 SEGV_ACCERR 000000000160
2026-04-23T20:44:32.9322010Z 4   Electron Framework  ... views::Widget::IsVisible() const + 28
2026-04-23T20:44:32.9528810Z 6   Electron Framework  ... electron::AutofillPopupView::Show() + 200
2026-04-23T20:44:32.9632090Z 7   Electron Framework  ... electron::AutofillPopup::CreateView(...) + 1380
2026-04-23T20:44:32.9749770Z 8   Electron Framework  ... electron::AutofillDriver::ShowAutofillPopup(...) + 736
2026-04-23T20:44:33.0015220Z ✗ Electron tests failed with kill signal SIGSEGV.
2026-04-27 10:49:00 +02:00
trop[bot]
57fee9f2c1 fix: remove insets on fullscreen windows on Windows (#51332)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Mitchell Cohen <mitch.cohen@me.com>
2026-04-26 19:13:13 -05:00
trop[bot]
d6bc5b6753 test: fix race in reentrant loadURL() ready-to-commit test (#51323)
test: fix race in reentrant loadURL() ready-to-commit test

Fix 'fails if loadurl is called after the navigation is ready to commit'
by using a done() callback to ensure the test waits for did-fail-load
before exiting.

Previously, the test would return and call afterEach(closeAllWindows),
potentially destroying the window while navigation was in flight.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
2026-04-26 12:30:12 -05:00
Keeley Hammond
51f35cf926 chore: cherry-pick 1 change from chromium (#51319) 2026-04-26 09:38:42 -05:00
reito
93cc936a94 fix: offscreen rendering with correct screen info. (#50375)
* fix: osr use correct screen info.

Co-authored-by: reito <reito@chromium.org>

* chore: e patches all (trivial only)

* 更新 breaking-changes.md

* chore: fixup .patches

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
Co-authored-by: John Kleinschmidt <jkleinsc@electronjs.org>
2026-04-24 12:36:54 -04:00
trop[bot]
48401169d9 build: restrict npm tarball contents to an explicit allowlist (#51305)
* build: restrict npm tarball contents to an explicit allowlist

The npm publish flow runs `npm pack` in a staging temp dir, but
`npm/package.json` had no `files` field — so any file that happened
to land in that dir was packed into the published tarball.

Recent releases (41.2.1+, 40.9.1+, 39.8.8+) shipped a self-referential
`.npm-cache/_logs/*-debug-0.log` (npm's own debug log, written into
the pack dir before pack finishes reading files) and a stray copy of
`SHASUMS256.txt` that duplicates the info already in `checksums.json`.

Add an explicit `files` allowlist so only the intended contents are
packaged, regardless of staging-dir contamination. `package.json`,
`README.md`, and `LICENSE` are auto-included by npm.

Fixes #51290.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Co-authored-by: Keeley Hammond <vertedinde@electronjs.org>

* build: include LICENSE and README.md in files allowlist

These are auto-included by npm regardless, but listing them makes the
intended contents of the tarball self-documenting alongside the other
entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Co-authored-by: Keeley Hammond <vertedinde@electronjs.org>

---------

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Keeley Hammond <vertedinde@electronjs.org>
2026-04-24 13:05:22 +00:00
Charles Kerr
c4965cb580 test: add linux coverage for default protocol client APIs (#51288)
Add Linux-only app tests to check the default protocol handler.
This includes adding reusable XDG mock fixtures.

Manual backport of 2c46abe from `main`.
2026-04-23 13:47:37 -05:00
trop[bot]
f24e43dc75 build: drop script/run-gn-format.py (#51282)
Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: David Sanders <dsanders11@ucsbalum.com>
2026-04-23 14:52:28 +02:00
Keeley Hammond
a3275d605c chore: cherry-pick 6 changes from chromium (41-x-y) (#51259)
* chore: cherry-pick 3 changes from chromium (41-x-y)

CVE-2026-5866: Use after free in Media
https://chromium-review.googlesource.com/c/chromium/src/+/7673253

CVE-2026-5867: Heap buffer overflow in WebML
https://chromium-review.googlesource.com/c/chromium/src/+/7677538

CVE-2026-5869: Heap buffer overflow in WebML
https://chromium-review.googlesource.com/c/chromium/src/+/7687895

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>

* chore: cherry-pick 3 changes from chromium

Backported security fixes for 493319454, 494158331, 496281816.

cherry-pick 7673406 from chromium
[WebNN] Reject fusing per-channel quantized gemm if the quantized dimension of filter is not 0
Bug: 493319454
Change-Id: Ib7e1236a535dc6a34d3ff9b9f0124a101bd89dbf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7673406

cherry-pick 7687618 from chromium
[WebNN] Prevent Pool2d indirection buffer overflow in TFLite
Bug: 494158331
Change-Id: I984556f0f608badf8f73fcbb096da5f41170a958
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7687618

cherry-pick 848cf5567223 from chromium
Check parent nodes when handling vector node insertions.
Fixed: 496281816
Change-Id: I0fc6956d1c09fcb7ea54d94819fdf1cb06fbd9e5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7705373

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>

* chore: reconcile build issues with patches

* chore: fixup patches

---------

Co-authored-by: Claude <svc-devxp-claude@slack-corp.com>
2026-04-23 01:05:16 -07:00
Samuel Attard
2674aa64b9 ci: centralize build-image SHA and pre-seed node-gyp headers (41-x-y) (#51275)
ci: centralize build-image SHA and pre-seed node-gyp headers (#51148)

* ci: centralize build-image SHA and pre-seed node-gyp headers

- Add .github/actions/build-image-sha as the single source of truth for
  the ghcr.io/electron/build (and arch-tagged electron/test) image SHA,
  with an optional override input for workflow_dispatch.
- Refactor build.yml, apply-patches.yml, build-git-cache.yml,
  clean-src-cache.yml, clean-orphaned-cache-uploads.yml, and the three
  publish workflows to resolve the SHA via a small ubuntu-slim setup job
  instead of hardcoding it in each file.
- Bump the image to daad061f (electron/build-images#68, which pre-warms
  the node-gyp header cache in the Linux images).
- Run the build.yml setup job on ubuntu-slim instead of ubuntu-latest.
- In install-dependencies (and the inline yarn installs in
  pipeline-electron-lint and generate-types), link deps with
  --mode=skip-build first, run `node-gyp install` with up to 3 retries
  (5s backoff) to populate the header cache, then run the build phase.
  This avoids the parallel-download race that intermittently fails the
  first native-addon configure with an empty common.gypi on cold
  macOS/Windows runners.

* ci: skip node-gyp header pre-seed on Linux

* ci: invoke node-gyp via its JS entrypoint for Windows compat

(cherry picked from commit f7ba34064e)
2026-04-23 09:54:07 +02:00
Keeley Hammond
47d85799a5 chore: cherry-pick 1 change from skia (#51264)
* chore: cherry-pick 8c705ac86366 from skia

Use SkSafeMath to prevent overflow in pixel offset calculations.

Bug:b/495534710
Change-Id: I0b2a684b5ad1105c7d25418556e40b4d9f511daf
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1481416
Commit-Queue: Stephen Nusko <nuskos@google.com>
Reviewed-by: Herb Derby <herb@google.com>

Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>

* chore: update patch

---------

Co-authored-by: Claude <svc-devxp-claude@slack-corp.com>
2026-04-23 09:50:22 +02:00
Samuel Attard
ce0739ad4f fix: ensure corsEnabled: false protocol handlers do not work across protocols (41-x-y) (#51270)
fix: ensure corsEnabled: false protocol handlers do not work across protocols (#51152)

* fix: ensure corsEnabled: false protocol handlers do not work across protocols

Subresource requests for registered custom protocols are routed to
ElectronURLLoaderFactory via the renderer's per-scheme URLLoaderFactoryBundle
entry, which bypasses the network service's CorsURLLoaderFactory. This meant a
cross-origin page could fetch() a scheme registered with {supportFetchAPI: true}
and read the response body even when {corsEnabled: true} was not set.

Replicate CorsURLLoader::StartRequest's kCorsDisabledScheme gate in
ElectronURLLoaderFactory::CreateLoaderAndStart so cross-origin mode=cors
requests to such schemes fail before the JS handler runs, and tag cross-origin
mode=no-cors responses as opaque so the body is not script-readable while <img>
and similar subresource loads continue to work.

Re-enable the long-disabled "disallows CORS and fetch requests when only
supportFetchAPI is specified" test, add coverage for the opaque/no-cors,
same-origin, handler-not-invoked, corsEnabled-unaffected and net.fetch-unaffected
cases, and migrate spec helpers that were exercising a {supportFetchAPI: true}
scheme cross-origin to a corsEnabled scheme.

* chore: oxfmt

(cherry picked from commit 92f0993d94)
2026-04-23 09:49:23 +02:00
David Sanders
285efaf87b build: don't use //third_party/depot_tools in lint.js (#51261)
build: don't use //third_party/depot_tools in lint.js (#51034)

* build: don't use //third_party/depot_tools in lint.js

* chore: also run python3 through depot tools
2026-04-22 16:58:23 -07:00
89 changed files with 2459 additions and 2178 deletions

View File

@@ -0,0 +1,24 @@
name: 'Build Image SHA'
description: 'Single source of truth for the ghcr.io/electron/build image SHA'
inputs:
override:
description: 'Optional override SHA (e.g. from a workflow_dispatch input)'
required: false
default: ''
outputs:
build-image-sha:
description: 'The electron/build image SHA to use'
value: ${{ steps.set.outputs.build-image-sha }}
runs:
using: 'composite'
steps:
- id: set
shell: bash
env:
OVERRIDE: ${{ inputs.override }}
run: |
if [ -n "$OVERRIDE" ]; then
echo "build-image-sha=$OVERRIDE" >> "$GITHUB_OUTPUT"
else
echo "build-image-sha=daad061f4b99a0ae1c841be4aa09188280a9c8a4" >> "$GITHUB_OUTPUT"
fi

View File

@@ -21,11 +21,28 @@ runs:
if [ "$TARGET_ARCH" = "x86" ]; then
export npm_config_arch="ia32"
fi
# if running on linux arm skip yarn Builds
ARCH=$(uname -m)
node script/yarn.js install --immutable --mode=skip-build
# if running on linux arm skip yarn Builds
if [ "$ARCH" = "armv7l" ]; then
echo "Skipping yarn build on linux arm"
node script/yarn.js install --immutable --mode=skip-build
else
# Pre-seed the node-gyp header cache so the parallel native-addon
# builds below don't race on a cold cache. Linux build containers
# already ship a warm cache (electron/build-images#68), so only do
# this on macOS / Windows runners.
if [ "$(uname -s)" != "Linux" ]; then
for i in 1 2 3; do
if node node_modules/node-gyp/bin/node-gyp.js install; then
break
fi
if [ "$i" = "3" ]; then
echo "node-gyp header pre-seed failed after 3 attempts" >&2
exit 1
fi
echo "node-gyp header pre-seed failed (attempt $i), retrying in 5s..." >&2
sleep 5
done
fi
node script/yarn.js install --immutable
fi

View File

@@ -0,0 +1,132 @@
From a8afee1089ec2ae9ab5837b438d07338aefb3bc4 Mon Sep 17 00:00:00 2001
From: Samuel Attard <sam@electronjs.org>
Date: Wed, 22 Apr 2026 16:27:51 -0700
Subject: [PATCH] siso: retry transient ERROR_INVALID_PARAMETER when opening
ninja files on Windows
ManifestParser.Load fans out across all subninja files (~90k in a
Chromium build) at NumCPU parallelism. On Windows builders where out/
is served through a filesystem filter driver (e.g. bindflt/wcifs for
container bind mounts), CreateFileW can intermittently return
ERROR_INVALID_PARAMETER under this concurrent open burst. The previous
patch removes the redundant per-chunk re-open, but the single remaining
open per file can still hit the race; without a retry a single transient
failure aborts the entire manifest load.
Wrap the remaining os.Open call in readFile in a small Windows-only
retry for ERROR_INVALID_PARAMETER (5 attempts, 5-80ms backoff). Each
retry is logged via clog.Warningf and also written to stderr so it is
visible in CI step output where glog warnings are file-only by default.
Other platforms keep the direct os.Open path.
---
siso/toolsupport/ninjautil/file_parser.go | 3 +-
siso/toolsupport/ninjautil/openfile_other.go | 18 +++++++
.../toolsupport/ninjautil/openfile_windows.go | 50 +++++++++++++++++++
3 files changed, 69 insertions(+), 2 deletions(-)
create mode 100644 siso/toolsupport/ninjautil/openfile_other.go
create mode 100644 siso/toolsupport/ninjautil/openfile_windows.go
diff --git a/siso/toolsupport/ninjautil/file_parser.go b/siso/toolsupport/ninjautil/file_parser.go
index 6311666..324528d 100644
--- a/siso/toolsupport/ninjautil/file_parser.go
+++ b/siso/toolsupport/ninjautil/file_parser.go
@@ -7,7 +7,6 @@ package ninjautil
import (
"context"
"fmt"
- "os"
"runtime/trace"
"sync"
"time"
@@ -91,7 +90,7 @@ func (p *fileParser) parseFile(ctx context.Context, fname string) error {
// readFile reads a file of fname in parallel.
func (p *fileParser) readFile(ctx context.Context, fname string) ([]byte, error) {
defer trace.StartRegion(ctx, "ninja.read").End()
- f, err := os.Open(fname)
+ f, err := openFile(ctx, fname)
if err != nil {
return nil, err
}
diff --git a/siso/toolsupport/ninjautil/openfile_other.go b/siso/toolsupport/ninjautil/openfile_other.go
new file mode 100644
index 0000000..9fca690
--- /dev/null
+++ b/siso/toolsupport/ninjautil/openfile_other.go
@@ -0,0 +1,18 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//go:build !windows
+
+package ninjautil
+
+import (
+ "context"
+ "os"
+)
+
+// openFile opens fname for reading.
+// See openfile_windows.go for the Windows variant with transient-error retry.
+func openFile(ctx context.Context, fname string) (*os.File, error) {
+ return os.Open(fname)
+}
diff --git a/siso/toolsupport/ninjautil/openfile_windows.go b/siso/toolsupport/ninjautil/openfile_windows.go
new file mode 100644
index 0000000..f9d8e9d
--- /dev/null
+++ b/siso/toolsupport/ninjautil/openfile_windows.go
@@ -0,0 +1,50 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//go:build windows
+
+package ninjautil
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "os"
+ "time"
+
+ "golang.org/x/sys/windows"
+
+ "go.chromium.org/build/siso/o11y/clog"
+)
+
+// openFile opens fname for reading, retrying transient
+// ERROR_INVALID_PARAMETER failures.
+//
+// On Windows, CreateFileW can intermittently return
+// ERROR_INVALID_PARAMETER when the target lives behind a filesystem
+// filter driver (e.g. bindflt/wcifs for container bind mounts) under
+// highly concurrent opens. loadFile fans out across ~90k subninja
+// files at NumCPU parallelism, so a single transient failure would
+// otherwise abort the whole manifest load.
+func openFile(ctx context.Context, fname string) (*os.File, error) {
+ const maxAttempts = 5
+ delay := 5 * time.Millisecond
+ for i := 0; ; i++ {
+ f, err := os.Open(fname)
+ if err == nil {
+ return f, nil
+ }
+ if i+1 >= maxAttempts || !errors.Is(err, windows.ERROR_INVALID_PARAMETER) {
+ return nil, err
+ }
+ clog.Warningf(ctx, "open %s: %v; retrying (%d/%d) after %s", fname, err, i+1, maxAttempts, delay)
+ fmt.Fprintf(os.Stderr, "siso: open %s: %v; retrying (%d/%d) after %s\n", fname, err, i+1, maxAttempts, delay)
+ select {
+ case <-time.After(delay):
+ case <-ctx.Done():
+ return nil, context.Cause(ctx)
+ }
+ delay *= 2
+ }
+}
--
2.53.0

View File

@@ -18,6 +18,7 @@ jobs:
pull-requests: read
outputs:
has-patches: ${{ steps.filter.outputs.patches }}
build-image-sha: ${{ steps.build-image-sha.outputs.build-image-sha }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
@@ -26,13 +27,16 @@ jobs:
# Use dorny/paths-filter instead of the path filter under the on: pull_request: block
# so that the output can be used to conditionally run the apply-patches job, which lets
# the job be marked as a required status check (conditional skip counts as a success).
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: filter
with:
filters: |
patches:
- DEPS
- 'patches/**'
- name: Set Build Image SHA
id: build-image-sha
uses: ./.github/actions/build-image-sha
apply-patches:
needs: setup
@@ -41,7 +45,7 @@ jobs:
permissions:
contents: read
container:
image: ghcr.io/electron/build:a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb
image: ghcr.io/electron/build:${{ needs.setup.outputs.build-image-sha }}
options: --user root
volumes:
- /mnt/cross-instance-cache:/mnt/cross-instance-cache

View File

@@ -6,8 +6,8 @@ on:
build-image-sha:
type: string
description: 'SHA for electron/build image'
default: 'a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb'
required: true
default: ''
required: false
skip-macos:
type: boolean
description: 'Skip macOS builds'
@@ -47,20 +47,21 @@ permissions: {}
jobs:
setup:
runs-on: ubuntu-latest
if: github.repository == 'electron/electron'
runs-on: ubuntu-slim
permissions:
contents: read
pull-requests: read
outputs:
docs: ${{ steps.filter.outputs.docs }}
src: ${{ steps.filter.outputs.src }}
build-image-sha: ${{ steps.set-output.outputs.build-image-sha }}
build-image-sha: ${{ steps.build-image-sha.outputs.build-image-sha }}
docs-only: ${{ steps.set-output.outputs.docs-only }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
ref: ${{ github.event.pull_request.head.sha }}
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: filter
with:
filters: |
@@ -72,14 +73,14 @@ jobs:
- CODE_OF_CONDUCT.md
src:
- '!docs/**'
- name: Set Outputs for Build Image SHA & Docs Only
- name: Set Build Image SHA
id: build-image-sha
uses: ./.github/actions/build-image-sha
with:
override: ${{ inputs.build-image-sha }}
- name: Set Docs Only
id: set-output
run: |
if [ -z "${{ inputs.build-image-sha }}" ]; then
echo "build-image-sha=a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb" >> "$GITHUB_OUTPUT"
else
echo "build-image-sha=${{ inputs.build-image-sha }}" >> "$GITHUB_OUTPUT"
fi
echo "docs-only=${{ steps.filter.outputs.docs == 'true' && steps.filter.outputs.src == 'false' }}" >> "$GITHUB_OUTPUT"
# Lint Jobs

View File

@@ -6,7 +6,8 @@ on:
build-image-sha:
type: string
description: 'SHA for electron/build image'
default: 'a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb'
default: ''
required: false
upload-to-storage:
description: 'Uploads to Azure storage'
required: false
@@ -20,12 +21,28 @@ on:
permissions: {}
jobs:
setup:
if: github.repository == 'electron/electron'
runs-on: ubuntu-slim
permissions:
contents: read
outputs:
build-image-sha: ${{ steps.build-image-sha.outputs.build-image-sha }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- name: Set Build Image SHA
id: build-image-sha
uses: ./.github/actions/build-image-sha
with:
override: ${{ inputs.build-image-sha }}
checkout-linux:
needs: setup
runs-on: electron-arc-centralus-linux-amd64-32core
permissions:
contents: read
container:
image: ghcr.io/electron/build:${{ inputs.build-image-sha }}
image: ghcr.io/electron/build:${{ needs.setup.outputs.build-image-sha }}
options: --user root
volumes:
- /mnt/cross-instance-cache:/mnt/cross-instance-cache
@@ -49,11 +66,11 @@ jobs:
attestations: write
contents: read
id-token: write
needs: checkout-linux
needs: [setup, checkout-linux]
with:
environment: production-release
build-runs-on: electron-arc-centralus-linux-amd64-32core
build-container: '{"image":"ghcr.io/electron/build:${{ inputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
build-container: '{"image":"ghcr.io/electron/build:${{ needs.setup.outputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
target-platform: linux
target-arch: x64
is-release: true
@@ -69,11 +86,11 @@ jobs:
attestations: write
contents: read
id-token: write
needs: checkout-linux
needs: [setup, checkout-linux]
with:
environment: production-release
build-runs-on: electron-arc-centralus-linux-amd64-32core
build-container: '{"image":"ghcr.io/electron/build:${{ inputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
build-container: '{"image":"ghcr.io/electron/build:${{ needs.setup.outputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
target-platform: linux
target-arch: arm
is-release: true
@@ -89,11 +106,11 @@ jobs:
attestations: write
contents: read
id-token: write
needs: checkout-linux
needs: [setup, checkout-linux]
with:
environment: production-release
build-runs-on: electron-arc-centralus-linux-amd64-32core
build-container: '{"image":"ghcr.io/electron/build:${{ inputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
build-container: '{"image":"ghcr.io/electron/build:${{ needs.setup.outputs.build-image-sha }}","options":"--user root","volumes":["/mnt/cross-instance-cache:/mnt/cross-instance-cache"]}'
target-platform: linux
target-arch: arm64
is-release: true

View File

@@ -6,8 +6,8 @@ on:
build-image-sha:
type: string
description: 'SHA for electron/build image'
default: 'a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb'
required: true
default: ''
required: false
upload-to-storage:
description: 'Uploads to Azure storage'
required: false
@@ -21,12 +21,28 @@ on:
permissions: {}
jobs:
setup:
if: github.repository == 'electron/electron'
runs-on: ubuntu-slim
permissions:
contents: read
outputs:
build-image-sha: ${{ steps.build-image-sha.outputs.build-image-sha }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- name: Set Build Image SHA
id: build-image-sha
uses: ./.github/actions/build-image-sha
with:
override: ${{ inputs.build-image-sha }}
checkout-macos:
needs: setup
runs-on: electron-arc-centralus-linux-amd64-32core
permissions:
contents: read
container:
image: ghcr.io/electron/build:${{ inputs.build-image-sha }}
image: ghcr.io/electron/build:${{ needs.setup.outputs.build-image-sha }}
options: --user root
volumes:
- /mnt/cross-instance-cache:/mnt/cross-instance-cache

View File

@@ -6,8 +6,8 @@ on:
build-image-sha:
type: string
description: 'SHA for electron/build image'
default: 'a82b87d7a4f5ff0cab61405f8151ac4cf4942aeb'
required: true
default: ''
required: false
upload-to-storage:
description: 'Uploads to Azure storage'
required: false
@@ -21,12 +21,28 @@ on:
permissions: {}
jobs:
setup:
if: github.repository == 'electron/electron'
runs-on: ubuntu-slim
permissions:
contents: read
outputs:
build-image-sha: ${{ steps.build-image-sha.outputs.build-image-sha }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
- name: Set Build Image SHA
id: build-image-sha
uses: ./.github/actions/build-image-sha
with:
override: ${{ inputs.build-image-sha }}
checkout-windows:
needs: setup
runs-on: electron-arc-centralus-linux-amd64-32core
permissions:
contents: read
container:
image: ghcr.io/electron/build:${{ inputs.build-image-sha }}
image: ghcr.io/electron/build:${{ needs.setup.outputs.build-image-sha }}
options: --user root --device /dev/fuse --cap-add SYS_ADMIN
volumes:
- /mnt/win-cache:/mnt/win-cache
@@ -36,8 +52,6 @@ jobs:
GCLIENT_EXTRA_ARGS: '--custom-var=checkout_win=True'
TARGET_OS: 'win'
ELECTRON_DEPOT_TOOLS_WIN_TOOLCHAIN: '1'
outputs:
build-image-sha: ${{ inputs.build-image-sha }}
steps:
- name: Checkout Electron
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
@@ -53,7 +67,7 @@ jobs:
# Build the patched siso binary in parallel with checkout-windows; the
# publish-*-win jobs consume it via SISO_PATH.
build-siso-windows:
if: github.repository == 'electron/electron'
needs: setup
uses: ./.github/workflows/pipeline-segment-build-siso-windows.yml
permissions:
contents: read

View File

@@ -458,8 +458,10 @@ source_set("electron_lib") {
"//components/certificate_transparency",
"//components/compose:buildflags",
"//components/embedder_support:user_agent",
"//components/heap_profiling/multi_process",
"//components/input",
"//components/language/core/browser",
"//components/memory_system",
"//components/net_log",
"//components/network_hints/browser",
"//components/network_hints/common:mojo_bindings",

2
DEPS
View File

@@ -2,7 +2,7 @@ gclient_gn_args_from = 'src'
vars = {
'chromium_version':
'146.0.7680.188',
'146.0.7680.216',
'node_version':
'v24.15.0',
'nan_version':

View File

@@ -65,6 +65,7 @@ template("electron_extra_paks") {
"$root_gen_dir/net/net_resources.pak",
"$root_gen_dir/third_party/blink/public/resources/blink_resources.pak",
"$root_gen_dir/third_party/blink/public/resources/inspector_overlay_resources.pak",
"$root_gen_dir/third_party/blink/public/strings/permission_element_generated_strings.pak",
"$target_gen_dir/electron_resources.pak",
]
deps = [
@@ -83,6 +84,7 @@ template("electron_extra_paks") {
"//net:net_resources",
"//third_party/blink/public:devtools_inspector_resources",
"//third_party/blink/public:resources",
"//third_party/blink/public/strings:permission_element_generated_strings",
"//ui/webui/resources",
]
if (defined(invoker.deps)) {
@@ -186,6 +188,7 @@ template("electron_paks") {
"${root_gen_dir}/extensions/strings/extensions_strings_",
"${root_gen_dir}/services/strings/services_strings_",
"${root_gen_dir}/third_party/blink/public/strings/blink_strings_",
"${root_gen_dir}/third_party/blink/public/strings/permission_element_strings_",
"${root_gen_dir}/ui/strings/app_locale_settings_",
"${root_gen_dir}/ui/strings/auto_image_annotation_strings_",
"${root_gen_dir}/ui/strings/ax_strings_",
@@ -202,6 +205,7 @@ template("electron_paks") {
"//extensions/strings",
"//services/strings",
"//third_party/blink/public/strings",
"//third_party/blink/public/strings:permission_element_strings",
"//ui/strings:app_locale_settings",
"//ui/strings:auto_image_annotation_strings",
"//ui/strings:ax_strings",

View File

@@ -124,4 +124,65 @@ Returns `Promise<Object>` - Resolves with an object containing the `value` and `
Get the maximum usage across processes of trace buffer as a percentage of the
full state.
### `contentTracing.enableHeapProfiling([options])` _Experimental_
<!--
```YAML history
added:
- pr-url: https://github.com/electron/electron/pull/50826
```
-->
* `options` ([EnableHeapProfilingOptions](structures/enable-heap-profiling-options.md)) (optional)
Returns `Promise<void>` - Resolves once heap profiling has been enabled.
Enable [heap profiling](https://chromium.googlesource.com/chromium/src/+/lkgr/docs/memory-infra/heap_profiler.md)
for MemoryInfra traces. Equivalent to the `--memlog` switch in Chrome.
Only takes effect if the `disabled-by-default-memory-infra` category is included.
Needs to be called before `contentTracing.startRecording()`.
Usage:
```js
const { contentTracing } = require('electron')
async function recordTrace () {
await contentTracing.enableHeapProfiling()
await contentTracing.startRecording({
included_categories: ['disabled-by-default-memory-infra'],
excluded_categories: ['*'],
memory_dump_config: {
triggers: [
{ mode: 'detailed', periodic_interval_ms: 1000 }
]
}
})
await new Promise(resolve => setTimeout(resolve, 5000))
const filePath = await contentTracing.stopRecording()
}
```
To view the recorded heap dumps:
1. Download the breakpad symbols for your Electron version from the Electron GitHub
[releases](https://github.com/electron/electron/releases)
2. Clone the [Electron source code](../development/build-instructions-gn.md)
3. In your Chromium checkout for Electron, run this command to symbolicate the heap dump:
```bash
python3 third_party/catapult/tracing/bin/symbolize_trace --use-breakpad-symbols --breakpad-symbols-directory /path/to/breakpad_symbols /path/to/trace.json
```
4. Open the symbolicated trace in `chrome://tracing` (the Perfetto UI does not support memory dumps
yet)
5. Click on one of the `M` symbols
6. Click on a `` triple bar icon (e.g., in the `malloc` column)
<img src="../images/viewing-heap-dumps.png" alt="Screenshot showing how to view a heapdump in Chromium's tracing view" />
[trace viewer]: https://chromium.googlesource.com/catapult/+/HEAD/tracing/README.md

View File

@@ -0,0 +1,26 @@
# EnableHeapProfilingOptions Object
* `mode` string (optional) - Controls which processes are profiled. Equivalent to `--memlog` in
Chrome. Default is `all`.
* `all` - Profile all processes.
* `browser` - Profile only the browser process.
* `gpu` - Profile only the GPU process.
* `minimal` - Profile only the browser and GPU processes.
* `renderer-sampling` - Profile at most 1 renderer process. Each renderer process has a fixed
probability of being profiled when the renderer process is started or, for existing processes,
when heap profiling is enabled.
* `all-renderers` - Profile all renderer processes.
* `utility-sampling` - Each utility process has a fixed probability of being profiled.
* `all-utilities` - Profile all utility processes.
* `utility-and-browser` - Profile all utility processes and the browser process.
* `samplingRate` number (optional) - Controls the sampling interval in bytes. The lower the
interval, the more precise the profile is. However it comes at the cost of performance. Default
is `100000` (100KB). That is enough to observe allocation sites that make allocations >500KB
total, where total equals to a single allocation size times the number of such allocations at the
same call site. Equivalent to `--memlog-sampling-rate` in Chrome. Must be an integer between
`1000` and `10000000`.
* `stackMode` string (optional) - Controls the type of metadata recorded for each allocation.
Equivalent to `--memlog-stack-mode` in Chrome. Default is `native`.
* `native` - Instruction addresses from unwinding the stack.
* `native-with-thread-names` - Instruction addresses from unwinding the stack. Includes the thread
name as the first frame.

View File

@@ -94,6 +94,7 @@
The actual output pixel format and color space of the texture should refer to [`OffscreenSharedTexture`](../structures/offscreen-shared-texture.md) object in the `paint` event.
* `argb` - The requested output texture format is 8-bit unorm RGBA, with SRGB SDR color space.
* `rgbaf16` - The requested output texture format is 16-bit float RGBA, with scRGB HDR color space.
* `deviceScaleFactor` number (optional) _Experimental_ - The device scale factor of the offscreen rendering output. If not set, will use primary display's scale factor as default.
* `contextIsolation` boolean (optional) - Whether to run Electron APIs and
the specified `preload` script in a separate JavaScript context. Defaults
to `true`. The context that the `preload` script runs in will only have

View File

@@ -226,7 +226,16 @@ Returns:
Only defined when the window is being created by a form that set
`target=_blank`.
* `disposition` string - Can be `default`, `foreground-tab`,
`background-tab`, `new-window` or `other`.
`background-tab`, `new-window` or `other`. Corresponds to the manner
an associated link was clicked. See Chromium's
[WindowOpenDisposition](https://source.chromium.org/chromium/chromium/src/+/main:ui/base/window_open_disposition.h).
* `default` - Indicates Chromium deems in-window navigation valid
for a window open call.
* `foreground-tab` - Corresponds to a left click or shift + middle click.
* `background-tab` - Corresponds to a middle click or ctrl/cmd + click.
* `new-window` - Corresponds to a shift + left click.
* `other` - A catch-all for the remaining Chromium dispositions not
handled by Electron.
Emitted _after_ successful creation of a window via `window.open` in the renderer.
Not emitted if the creation of the window is canceled from
@@ -1449,8 +1458,17 @@ Ignore application menu shortcuts while this web contents is focused.
* `url` string - The _resolved_ version of the URL passed to `window.open()`. e.g. opening a window with `window.open('foo')` will yield something like `https://the-origin/the/current/path/foo`.
* `frameName` string - Name of the window provided in `window.open()`
* `features` string - Comma separated list of window features provided to `window.open()`.
* `disposition` string - Can be `default`, `foreground-tab`, `background-tab`,
`new-window` or `other`.
* `disposition` string - Can be `default`, `foreground-tab`,
`background-tab`, `new-window` or `other`. Corresponds to the manner
an associated link was clicked. See Chromium's
[WindowOpenDisposition](https://source.chromium.org/chromium/chromium/src/+/main:ui/base/window_open_disposition.h).
* `default` - Indicates Chromium deems in-window navigation valid
for a window open call.
* `foreground-tab` - Corresponds to a left click or shift + middle click.
* `background-tab` - Corresponds to a middle click or ctrl/cmd + click.
* `new-window` - Corresponds to a shift + left click.
* `other` - A catch-all for the remaining Chromium dispositions not
handled by Electron.
* `referrer` [Referrer](structures/referrer.md) - The referrer that will be
passed to the new window. May or may not result in the `Referer` header being
sent, depending on the referrer policy.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -24,6 +24,27 @@ npx electron .
The above command will run the current working directory with Electron. Note that
any dependencies in your app will not be installed.
## Installing prereleases
Electron [distributes experimental releases of future major versions](./electron-timelines.md)
via npm as well.
Nightly builds contain the latest changes from the `main` branch:
```sh
npm install electron-nightly --save-dev
```
Alpha and beta builds contain changes slated for the next major version:
```sh
npm install electron@alpha --save-dev
npm install electron@beta --save-dev
```
> [!TIP]
> For more information on available Electron releases, see the [Release Status dashboard](https://releases.electronjs.org).
## Customization
If you want to change the architecture that is downloaded (e.g., `x64` on an

View File

@@ -90,6 +90,7 @@ auto_filenames = {
"docs/api/structures/custom-scheme.md",
"docs/api/structures/desktop-capturer-source.md",
"docs/api/structures/display.md",
"docs/api/structures/enable-heap-profiling-options.md",
"docs/api/structures/extension-info.md",
"docs/api/structures/extension.md",
"docs/api/structures/file-filter.md",

View File

@@ -7,6 +7,16 @@
"scripts": {
"postinstall": "node install.js"
},
"files": [
"LICENSE",
"README.md",
"abi_version",
"checksums.json",
"cli.js",
"electron.d.ts",
"index.js",
"install.js"
],
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^24.9.0",

View File

@@ -86,7 +86,7 @@
"gn-typescript-definitions": "npm run create-typescript-definitions && node script/cp.mjs electron.d.ts",
"pre-flight": "pre-flight",
"gn-check": "node ./script/gn-check.js",
"gn-format": "python3 script/run-gn-format.py",
"gn-format": "node ./script/lint.js --gn --fix",
"precommit": "lint-staged",
"preinstall": "node -e 'process.exit(0)'",
"pretest": "npm run create-typescript-definitions",
@@ -121,7 +121,7 @@
],
"*.{gn,gni}": [
"npm run gn-check",
"npm run gn-format"
"node ./script/lint.js --gn --fix --only --"
],
"*.py": [
"node script/lint.js --py --fix --only --"

View File

@@ -157,16 +157,19 @@ fix_initialize_com_on_desktopmedialistcapturethread_on_windows.patch
fix_use_fresh_lazynow_for_onendworkitemimpl_after_didruntask.patch
cherry-pick-4073d491fb55.patch
cherry-pick-8c1ead5a699f.patch
cherry-pick-8b08fb7c9dce.patch
cherry-pick-be87466afecb.patch
cherry-pick-c215f8e6f049.patch
cherry-pick-a6357144e7bf.patch
cherry-pick-41bfbc009df8.patch
cherry-pick-4002a66778d2.patch
cherry-pick-23865499a86a.patch
cherry-pick-c81f01b469c4.patch
cherry-pick-1b69067db7d2.patch
cherry-pick-d513cd2fe668.patch
cherry-pick-bb8d4c29dfdb.patch
cherry-pick-847b11ad2fa3.patch
cherry-pick-eeb3e031eb89.patch
cherry-pick-fccaeb9e0967.patch
cherry-pick-d141d62357df.patch
cherry-pick-c75f63de7188.patch
cherry-pick-7687618.patch
patch_osr_control_screen_info.patch
cherry-pick-cve-2026-6920.patch
fix_make_macos_text_replacement_work_on_contenteditable.patch
fix-dcheck-failure-when-starting-heap-profiler-for-renderer.patch

View File

@@ -1,224 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Fergal Daly <fergal@chromium.org>
Date: Sun, 12 Apr 2026 20:37:39 -0700
Subject: [M146] Fix UAF in FileSystemAccessChangeSource.
Original change's description:
> Fix UAF in FileSystemAccessChangeSource.
>
> `DidInitialize` calls any outstanding initialization callbacks but a
> callback can delete this. The code guards against this in its access
> of `initialization_callbacks_` but not `initialization_result_`.
>
> This fix keeps a copy of the result on the stack.
>
> This also adds a test which fails with ASAN before the fix is applied
> and passes after.
>
> The basic test code was written by Gemini.
>
> Fixed: 497880137
> Change-Id: I046831db23cb4b8e41964910e2aede9b1be0db7f
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7728464
> Auto-Submit: Fergal Daly <fergal@chromium.org>
> Reviewed-by: Ming-Ying Chung <mych@chromium.org>
> Commit-Queue: Ming-Ying Chung <mych@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1610499}
(cherry picked from commit c0390bcd64ba1fd6594fbc9f6246a1649662d683)
Bug: 500247135,497880137
Change-Id: I046831db23cb4b8e41964910e2aede9b1be0db7f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7754020
Commit-Queue: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
Auto-Submit: Chrome Cherry Picker <chrome-cherry-picker@chops-service-accounts.iam.gserviceaccount.com>
Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
Cr-Commit-Position: refs/branch-heads/7680@{#3929}
Cr-Branched-From: 76b7d80e5cda23fe6537eed26d68c92e995c7f39-refs/heads/main@{#1582197}
diff --git a/content/browser/file_system_access/file_system_access_change_source.cc b/content/browser/file_system_access/file_system_access_change_source.cc
index 566dc1ea40b43a54b33d70e82a20ff5695b57b5e..48bd867a9d3d140eaf515ea7bc1613231f7e79e9 100644
--- a/content/browser/file_system_access/file_system_access_change_source.cc
+++ b/content/browser/file_system_access/file_system_access_change_source.cc
@@ -71,13 +71,14 @@ void FileSystemAccessChangeSource::DidInitialize(
CHECK(!initialization_result_.has_value());
CHECK(!initialization_callbacks_.empty());
- initialization_result_ = std::move(result);
+ // The callbacks may cause |this| to be deleted, so we should only use
+ // stack-based objects below.
+ initialization_result_ = result->Clone();
- // Move the callbacks to the stack since they may cause |this| to be deleted.
auto initialization_callbacks = std::move(initialization_callbacks_);
initialization_callbacks_.clear();
for (auto& callback : initialization_callbacks) {
- std::move(callback).Run(initialization_result_->Clone());
+ std::move(callback).Run(result->Clone());
}
}
diff --git a/content/browser/file_system_access/file_system_access_change_source_unittest.cc b/content/browser/file_system_access/file_system_access_change_source_unittest.cc
new file mode 100644
index 0000000000000000000000000000000000000000..b0f15909bebda29fc2ec689a6d3b15d797dcc722
--- /dev/null
+++ b/content/browser/file_system_access/file_system_access_change_source_unittest.cc
@@ -0,0 +1,146 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/file_system_access/file_system_access_change_source.h"
+
+#include "base/files/scoped_temp_dir.h"
+#include "base/functional/bind.h"
+#include "base/memory/scoped_refptr.h"
+#include "base/task/sequenced_task_runner.h"
+#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
+#include "content/browser/file_system_access/file_system_access_watch_scope.h"
+#include "storage/browser/file_system/file_system_context.h"
+#include "storage/browser/file_system/file_system_url.h"
+#include "storage/browser/quota/quota_manager_proxy.h"
+#include "storage/browser/test/test_file_system_context.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/mojom/file_system_access/file_system_access_error.mojom.h"
+
+namespace content {
+
+namespace {
+
+class MockRawChangeObserver
+ : public FileSystemAccessChangeSource::RawChangeObserver {
+ public:
+ MOCK_METHOD(void,
+ OnRawChange,
+ (const storage::FileSystemURL& changed_url,
+ bool error,
+ const FileSystemAccessChangeSource::ChangeInfo& change_info,
+ const FileSystemAccessWatchScope& scope),
+ (override));
+ MOCK_METHOD(void,
+ OnUsageChange,
+ (size_t old_usage,
+ size_t new_usage,
+ const FileSystemAccessWatchScope& scope),
+ (override));
+ MOCK_METHOD(void,
+ OnSourceBeingDestroyed,
+ (FileSystemAccessChangeSource * source),
+ (override));
+};
+
+class FakeChangeSource : public FileSystemAccessChangeSource {
+ public:
+ FakeChangeSource(
+ FileSystemAccessWatchScope scope,
+ scoped_refptr<storage::FileSystemContext> file_system_context)
+ : FileSystemAccessChangeSource(std::move(scope),
+ std::move(file_system_context)) {}
+ ~FakeChangeSource() override = default;
+
+ // FileSystemAccessChangeSource:
+ void Initialize(
+ base::OnceCallback<void(blink::mojom::FileSystemAccessErrorPtr)>
+ on_source_initialized) override {
+ base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, base::BindOnce(std::move(on_source_initialized),
+ blink::mojom::FileSystemAccessError::New(
+ blink::mojom::FileSystemAccessStatus::kOk,
+ base::File::FILE_OK, "")));
+ }
+
+ void Signal(const storage::FileSystemURL& changed_url,
+ bool error = false,
+ ChangeInfo change_info = ChangeInfo()) {
+ NotifyOfChange(changed_url, error, change_info);
+ }
+};
+
+} // namespace
+
+class FileSystemAccessChangeSourceTest : public testing::Test {
+ public:
+ FileSystemAccessChangeSourceTest()
+ : task_environment_(base::test::TaskEnvironment::MainThreadType::IO) {}
+
+ void SetUp() override {
+ ASSERT_TRUE(dir_.CreateUniqueTempDir());
+ file_system_context_ = storage::CreateFileSystemContextForTesting(
+ /*quota_manager_proxy=*/nullptr, dir_.GetPath());
+ }
+
+ protected:
+ base::test::TaskEnvironment task_environment_;
+ base::ScopedTempDir dir_;
+ scoped_refptr<storage::FileSystemContext> file_system_context_;
+};
+
+TEST_F(FileSystemAccessChangeSourceTest, CreateAndInitialize) {
+ auto file_path = dir_.GetPath().AppendASCII("file");
+ auto file_url = file_system_context_->CreateCrackedFileSystemURL(
+ blink::StorageKey(), storage::kFileSystemTypeLocal, file_path);
+
+ auto scope = FileSystemAccessWatchScope::GetScopeForFileWatch(file_url);
+ FakeChangeSource source(scope, file_system_context_);
+
+ base::test::TestFuture<blink::mojom::FileSystemAccessErrorPtr> future;
+ source.EnsureInitialized(future.GetCallback());
+ EXPECT_EQ(future.Get()->status, blink::mojom::FileSystemAccessStatus::kOk);
+}
+
+TEST_F(FileSystemAccessChangeSourceTest, NotifyOfChange) {
+ auto file_path = dir_.GetPath().AppendASCII("file");
+ auto file_url = file_system_context_->CreateCrackedFileSystemURL(
+ blink::StorageKey(), storage::kFileSystemTypeLocal, file_path);
+
+ auto scope = FileSystemAccessWatchScope::GetScopeForFileWatch(file_url);
+ FakeChangeSource source(scope, file_system_context_);
+
+ MockRawChangeObserver observer;
+ source.AddObserver(&observer);
+
+ EXPECT_CALL(observer, OnRawChange(testing::Eq(file_url), testing::IsFalse(),
+ testing::_, testing::Eq(scope)));
+ source.Signal(file_url);
+
+ source.RemoveObserver(&observer);
+}
+
+// A callback passed to `EnsureInitialized` may result in `this` being
+// destroyed. This tests that `DidInitialize` (which calls the callbacks) is
+// robust to that situation. See https://crbug.com/497880137.
+TEST_F(FileSystemAccessChangeSourceTest, TestDestroyFromInitializeCallback) {
+ auto file_path = dir_.GetPath().AppendASCII("file");
+ auto file_url = file_system_context_->CreateCrackedFileSystemURL(
+ blink::StorageKey(), storage::kFileSystemTypeLocal, file_path);
+
+ auto scope = FileSystemAccessWatchScope::GetScopeForFileWatch(file_url);
+ FakeChangeSource* source = new FakeChangeSource(scope, file_system_context_);
+
+ source->EnsureInitialized(base::BindOnce(
+ [](FakeChangeSource* source, blink::mojom::FileSystemAccessErrorPtr) {
+ delete source;
+ },
+ base::Unretained(source)));
+ base::test::TestFuture<blink::mojom::FileSystemAccessErrorPtr> future;
+ source->EnsureInitialized(future.GetCallback());
+ EXPECT_EQ(future.Get()->status, blink::mojom::FileSystemAccessStatus::kOk);
+}
+
+} // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 07cbf495717714d71d977a8820e08050c3062526..f5d72a89c7229bf8e897c90660feca482ac82594 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -2656,6 +2656,7 @@ test("content_unittests") {
"../browser/fenced_frame/redacted_fenced_frame_config_mojom_traits_unittest.cc",
"../browser/file_system/browser_file_system_helper_unittest.cc",
"../browser/file_system/file_system_operation_runner_unittest.cc",
+ "../browser/file_system_access/file_system_access_change_source_unittest.cc",
"../browser/file_system_access/file_system_access_directory_handle_impl_unittest.cc",
"../browser/file_system_access/file_system_access_file_handle_impl_unittest.cc",
"../browser/file_system_access/file_system_access_file_modification_host_impl_unittest.cc",

View File

@@ -0,0 +1,43 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Wei Wang <wei4.wang@intel.com>
Date: Fri, 20 Mar 2026 19:57:55 -0700
Subject: [WebNN] Prevent Pool2d indirection buffer overflow in TFLite
Add a check to ensure the size of the internal indirection buffer used
by TFLite's Pool2d implementation does not exceed the maximum value
of a size_t integer.
Bug: 494158331
Change-Id: I984556f0f608badf8f73fcbb096da5f41170a958
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7687618
Reviewed-by: Hu, Ningxin <ningxin.hu@intel.com>
Reviewed-by: Reilly Grant <reillyg@chromium.org>
Commit-Queue: Wang, Wei4 <wei4.wang@intel.com>
Cr-Commit-Position: refs/heads/main@{#1602966}
diff --git a/services/webnn/tflite/graph_builder_tflite.cc b/services/webnn/tflite/graph_builder_tflite.cc
index 578627bf9b42885d170ec0b83b581e2b09984823..cb5312511ef372a98a2bd8d34ed8e39e8308cee6 100644
--- a/services/webnn/tflite/graph_builder_tflite.cc
+++ b/services/webnn/tflite/graph_builder_tflite.cc
@@ -6886,6 +6886,21 @@ auto GraphBuilderTflite::SerializePool2d(const mojom::Pool2d& pool2d)
CHECK_EQ(input_shape.size(), 4u);
const webnn::Size2d<uint32_t> input_size2d = {.height = input_shape[1],
.width = input_shape[2]};
+
+ // TODO(crbug.com/493988762): Explicitly restrict to int32_t in the WebNN spec
+ // or opSupportLimits for synchronous frontend validation.
+ if (!base::IsValueInRangeForNumericType<int32_t>(pool2d.strides->height) ||
+ !base::IsValueInRangeForNumericType<int32_t>(pool2d.strides->width)) {
+ return base::unexpected(
+ "Stride width and height must fit within the int32 range");
+ }
+
+ if (!base::IsValueInRangeForNumericType<int32_t>(filter_size2d.height) ||
+ !base::IsValueInRangeForNumericType<int32_t>(filter_size2d.width)) {
+ return base::unexpected(
+ "Filter width and height must fit within the int32 range");
+ }
+
ASSIGN_OR_RETURN(TfLitePadding padding_mode,
GetPool2dTfLitePaddingMode(
*pool2d.padding, input_size2d, filter_size2d,

View File

@@ -1,67 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: p0-tato <smartphonewithbear@gmail.com>
Date: Tue, 14 Apr 2026 13:14:30 -0700
Subject: [M146] Fix dangling pointers in OpenXrSpatialFrameworkManager
Original change's description:
> Fix dangling pointers in OpenXrSpatialFrameworkManager
>
> Pointers to vector elements were collected during emplace_back,
> which invalidates them on reallocation. Split into two loops
> and reserve the correct capacity.
>
> Bug: 497724498
> Change-Id: I204534bc1bd1522fe03db86f03c2c3e0d285631c
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7735242
> Commit-Queue: Brian Sheedy <bsheedy@chromium.org>
> Reviewed-by: Brian Sheedy <bsheedy@chromium.org>
> Reviewed-by: Brandon Jones <bajones@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1613990}
(cherry picked from commit b173791bf4026a6bb43124f7c5f46cfa4539c014)
Bug: 502440265,497724498
Change-Id: I204534bc1bd1522fe03db86f03c2c3e0d285631c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7759844
Auto-Submit: chrome-cherry-picker@chops-service-accounts.iam.gserviceaccount.com <chrome-cherry-picker@chops-service-accounts.iam.gserviceaccount.com>
Bot-Commit: rubber-stamper@appspot.gserviceaccount.com <rubber-stamper@appspot.gserviceaccount.com>
Commit-Queue: rubber-stamper@appspot.gserviceaccount.com <rubber-stamper@appspot.gserviceaccount.com>
Cr-Commit-Position: refs/branch-heads/7680@{#3944}
Cr-Branched-From: 76b7d80e5cda23fe6537eed26d68c92e995c7f39-refs/heads/main@{#1582197}
diff --git a/AUTHORS b/AUTHORS
index 7cc777b399ab46f88b6b1809bf6fd0cb22170694..505480b09c1d41b1facf4e2b165bad86b1815127 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -729,6 +729,7 @@ Jihoon Chung <jihoon@gmail.com>
Jihun Brent Kim <devgrapher@gmail.com>
Jihwan Marc Kim <bluewhale.marc@gmail.com>
Jihye Hyun <jijinny26@gmail.com>
+Jihyeon Jeong <smartphonewithbear@gmail.com>
Jihyeon Lee <wlgus7464@gmail.com>
Jim Wu <lofoz.tw@gmail.com>
Jin Yang <jin.a.yang@intel.com>
diff --git a/device/vr/openxr/openxr_spatial_framework_manager.cc b/device/vr/openxr/openxr_spatial_framework_manager.cc
index 520f25230c427bf775333910530d1ad841f3ad71..5c93d694aa5a2259c683f1d521611046293195a2 100644
--- a/device/vr/openxr/openxr_spatial_framework_manager.cc
+++ b/device/vr/openxr/openxr_spatial_framework_manager.cc
@@ -71,12 +71,15 @@ OpenXrSpatialFrameworkManager::OpenXrSpatialFrameworkManager(
// to help abstract some of the details of creating the child structs, even
// though at present we only have a configuration base.
std::vector<OpenXrSpatialCapabilityConfigurationBase> capability_configs;
- std::vector<XrSpatialCapabilityConfigurationBaseHeaderEXT*>
- capability_config_ptrs;
+ capability_configs.reserve(capability_configuration.size());
for (auto& [capability, components] : capability_configuration) {
capability_configs.emplace_back(capability, components);
- capability_config_ptrs.push_back(
- capability_configs.back().GetAsBaseHeader());
+ }
+
+ std::vector<XrSpatialCapabilityConfigurationBaseHeaderEXT*>
+ capability_config_ptrs;
+ for (auto& config : capability_configs) {
+ capability_config_ptrs.push_back(config.GetAsBaseHeader());
}
XrSpatialContextCreateInfoEXT create_info = {

View File

@@ -1,94 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Sunny Sachanandani <sunnyps@chromium.org>
Date: Fri, 10 Apr 2026 23:37:43 -0700
Subject: [M146] [gpu] Fix OOB write due to unvalidated get_offset
Original change's description:
> [gpu] Fix OOB write due to unvalidated get_offset
>
> A compromised GPU process can provide an invalid get_offset to the
> CommandBufferHelper (e.g., via shared memory). This offset is used to
> calculate available space and could lead to out-of-bounds writes in the
> Browser process if not validated.
>
> This change adds a bounds check in
> CommandBufferHelper::UpdateCachedState to ensure that the cached
> get_offset is within the valid range [0, total_entry_count_]. If an
> invalid offset is detected, it forces a context loss, frees the ring
> buffer, and marks the helper as unusable, preventing further operations.
>
> Bug: 498782145
> Test: CommandBufferHelperTest.*
> Change-Id: I8c64e546ecdc90a5a22d15e57ff762a86a6a6964
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7739951
> Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
> Auto-Submit: Sunny Sachanandani <sunnyps@chromium.org>
> Commit-Queue: Sunny Sachanandani <sunnyps@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1611853}
(cherry picked from commit dc5e20c4c055d6952854a566d520211c6d505f74)
Bug: 498782145
Fixed: 500956607
Change-Id: Ia726612e0a930ee79460fbd7d795afa4d94e2a7b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7745786
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
Auto-Submit: Sunny Sachanandani <sunnyps@chromium.org>
Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
Commit-Queue: Sunny Sachanandani <sunnyps@chromium.org>
Cr-Commit-Position: refs/branch-heads/7680@{#3919}
Cr-Branched-From: 76b7d80e5cda23fe6537eed26d68c92e995c7f39-refs/heads/main@{#1582197}
diff --git a/gpu/command_buffer/client/cmd_buffer_helper.cc b/gpu/command_buffer/client/cmd_buffer_helper.cc
index ccda45b133c6a9f2ee60ccc8900bd4a4ce328394..5aea0c81b29b3507099f399c374f3cb372a3100e 100644
--- a/gpu/command_buffer/client/cmd_buffer_helper.cc
+++ b/gpu/command_buffer/client/cmd_buffer_helper.cc
@@ -158,6 +158,17 @@ void CommandBufferHelper::UpdateCachedState(const CommandBuffer::State& state) {
service_on_old_buffer_ =
(state.set_get_buffer_count != set_get_buffer_count_);
cached_get_offset_ = service_on_old_buffer_ ? 0 : state.get_offset;
+
+ if (!service_on_old_buffer_ &&
+ (cached_get_offset_ < 0 || cached_get_offset_ > total_entry_count_)) {
+ command_buffer_->ForceLostContext(error::kGuilty);
+ FreeRingBuffer();
+ usable_ = false;
+ context_lost_ = true;
+ cached_get_offset_ = 0; // Safe fallback
+ return;
+ }
+
cached_last_token_read_ = state.token;
// Don't transition from a lost context to a working context.
context_lost_ |= error::IsError(state.error);
diff --git a/gpu/command_buffer/client/cmd_buffer_helper_test.cc b/gpu/command_buffer/client/cmd_buffer_helper_test.cc
index 1b9254d318ae770ca980d2fed1399a69438afa10..009a87e8bf7a3475f63cd51206868dec187f5e06 100644
--- a/gpu/command_buffer/client/cmd_buffer_helper_test.cc
+++ b/gpu/command_buffer/client/cmd_buffer_helper_test.cc
@@ -67,6 +67,8 @@ class CommandBufferHelperTest : public testing::Test {
return helper_->immediate_entry_count_;
}
+ int32_t TotalEntryCount() const { return helper_->total_entry_count_; }
+
// Adds a command to the buffer through the helper, while adding it as an
// expected call on the API mock.
void AddCommandWithExpect(error::Error _return,
@@ -655,6 +657,17 @@ TEST_F(CommandBufferHelperTest, IsContextLost) {
EXPECT_TRUE(helper_->IsContextLost());
}
+TEST_F(CommandBufferHelperTest, TestInvalidGetOffset) {
+ EXPECT_FALSE(helper_->IsContextLost());
+ EXPECT_TRUE(helper_->usable());
+
+ command_buffer_->SetGetOffsetForTest(TotalEntryCount() + 1);
+ helper_->RefreshCachedToken(); // calls UpdateCachedState internally.
+
+ EXPECT_TRUE(helper_->IsContextLost());
+ EXPECT_FALSE(helper_->usable());
+}
+
// Checks helper's 'flush generation' updates.
TEST_F(CommandBufferHelperTest, TestFlushGeneration) {
// Explicit flushing only.

View File

@@ -1,224 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jonathan Ross <jonross@chromium.org>
Date: Wed, 8 Apr 2026 17:15:45 -0700
Subject: gl: Make DCOMPSurfaceRegistry thread-safe
DCOMPSurfaceRegistry is accessed from both the GPU IO thread (via
GpuServiceImpl) and the GPU main scheduler thread (via DCOMPTexture).
The underlying base::flat_map is not thread-safe, leading to potential
container corruption and crashes (UAF, BOf) during concurrent access.
This CL adds a base::Lock to protect all accesses to the map and
includes a new multi-threaded stress test to verify the fix.
Bug: 493315759
Change-Id: Ibb7ef5e602f222410fde06a61fb3f5e571e7a70f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7737061
Reviewed-by: Sunny Sachanandani <sunnyps@chromium.org>
Commit-Queue: Jonathan Ross <jonross@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1611867}
diff --git a/ui/gl/BUILD.gn b/ui/gl/BUILD.gn
index 3584b693370b5199456608a26ceb763f6e9c3446..1cb66199a0b8adf2035a05fecc411c67180f7e80 100644
--- a/ui/gl/BUILD.gn
+++ b/ui/gl/BUILD.gn
@@ -552,6 +552,7 @@ test("gl_unittests") {
if (is_win) {
sources += [
"dcomp_presenter_unittest.cc",
+ "dcomp_surface_registry_unittest.cc",
"delegated_ink_point_renderer_gpu_unittest.cc",
"gl_fence_win_unittest.cc",
"hdr_metadata_helper_win_unittest.cc",
diff --git a/ui/gl/dcomp_surface_registry.cc b/ui/gl/dcomp_surface_registry.cc
index 352cc298b9ea97361ae2a7d668b7d7e9eb455cd5..410f76f8980438abae32b6c89e7083ae48cf1699 100644
--- a/ui/gl/dcomp_surface_registry.cc
+++ b/ui/gl/dcomp_surface_registry.cc
@@ -3,8 +3,11 @@
// found in the LICENSE file.
#include "ui/gl/dcomp_surface_registry.h"
+
+#include "base/check.h"
#include "base/logging.h"
#include "base/no_destructor.h"
+#include "base/synchronization/lock.h"
namespace gl {
@@ -20,8 +23,11 @@ base::UnguessableToken DCOMPSurfaceRegistry::RegisterDCOMPSurfaceHandle(
base::win::ScopedHandle surface) {
DVLOG(1) << __func__;
base::UnguessableToken token = base::UnguessableToken::Create();
- DCHECK(surface_handle_map_.find(token) == surface_handle_map_.end());
- surface_handle_map_[token] = std::move(surface);
+ {
+ base::AutoLock lock(lock_);
+ DCHECK(surface_handle_map_.find(token) == surface_handle_map_.end());
+ surface_handle_map_[token] = std::move(surface);
+ }
DVLOG(1) << __func__ << ": Surface handle registered with token " << token;
return token;
}
@@ -29,12 +35,14 @@ base::UnguessableToken DCOMPSurfaceRegistry::RegisterDCOMPSurfaceHandle(
void DCOMPSurfaceRegistry::UnregisterDCOMPSurfaceHandle(
const base::UnguessableToken& token) {
DVLOG(1) << __func__;
+ base::AutoLock lock(lock_);
surface_handle_map_.erase(token);
}
base::win::ScopedHandle DCOMPSurfaceRegistry::TakeDCOMPSurfaceHandle(
const base::UnguessableToken& token) {
DVLOG(1) << __func__;
+ base::AutoLock lock(lock_);
auto surface_iter = surface_handle_map_.find(token);
if (surface_iter != surface_handle_map_.end()) {
// Take ownership.
diff --git a/ui/gl/dcomp_surface_registry.h b/ui/gl/dcomp_surface_registry.h
index 803a3cc6398f0777504063118920998869086d7f..7cd9fdbfe8669bc97d4b664fdb29573ec2ea26de 100644
--- a/ui/gl/dcomp_surface_registry.h
+++ b/ui/gl/dcomp_surface_registry.h
@@ -7,6 +7,7 @@
#include "base/containers/flat_map.h"
#include "base/no_destructor.h"
+#include "base/synchronization/lock.h"
#include "base/unguessable_token.h"
#include "base/win/scoped_handle.h"
#include "ui/gl/gl_export.h"
@@ -44,7 +45,9 @@ class GL_EXPORT DCOMPSurfaceRegistry {
~DCOMPSurfaceRegistry();
base::flat_map<base::UnguessableToken, base::win::ScopedHandle>
- surface_handle_map_;
+ surface_handle_map_ GUARDED_BY(lock_);
+
+ base::Lock lock_;
};
} // namespace gl
diff --git a/ui/gl/dcomp_surface_registry_unittest.cc b/ui/gl/dcomp_surface_registry_unittest.cc
new file mode 100644
index 0000000000000000000000000000000000000000..595e2388e9f50df33214359ecef0c135d94610b8
--- /dev/null
+++ b/ui/gl/dcomp_surface_registry_unittest.cc
@@ -0,0 +1,118 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ui/gl/dcomp_surface_registry.h"
+
+#include <windows.h>
+
+#include <atomic>
+#include <thread>
+#include <vector>
+
+#include "base/memory/raw_ptr.h"
+#include "base/synchronization/lock.h"
+#include "base/unguessable_token.h"
+#include "base/win/scoped_handle.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace gl {
+
+namespace {
+
+class DCOMPSurfaceRegistryTest : public testing::Test {
+ public:
+ void SetUp() override { registry_ = DCOMPSurfaceRegistry::GetInstance(); }
+
+ protected:
+ raw_ptr<DCOMPSurfaceRegistry> registry_;
+};
+
+} // namespace
+
+// Stress test for concurrent access to DCOMPSurfaceRegistry using the
+// barrier pattern to ensure TSAN consistently catches data races.
+//
+// Without proper synchronization (e.g., base::Lock), this test would likely
+// fail in the following ways:
+// 1. Memory Corruption (UAF/HeapBOf): base::flat_map uses a contiguous
+// std::vector. If one thread triggers a reallocation during an insertion
+// while another thread is searching or erasing, the latter will hold an
+// invalidated iterator or pointer.
+// 2. Container Inconsistency: Concurrent insertions and erasures can leave
+// the map in an unsorted or corrupted state, leading to failed lookups
+// for valid tokens.
+// 3. Sanitizer Triggers: ASan would detect container-overflow or
+// heap-use-after-free, and TSan would flag a data race.
+TEST_F(DCOMPSurfaceRegistryTest, ConcurrentRegisterAndTake) {
+ const int kOpsPerThread = 100;
+
+ std::vector<base::UnguessableToken> tokens;
+ base::Lock tokens_lock;
+
+ std::atomic<bool> start_flag{false};
+ std::atomic<int> threads_ready{0};
+
+ auto register_worker = [&]() {
+ threads_ready++;
+ while (!start_flag.load(std::memory_order_acquire)) {
+ std::this_thread::yield();
+ }
+
+ for (int i = 0; i < kOpsPerThread; ++i) {
+ base::win::ScopedHandle handle(
+ ::CreateEvent(nullptr, FALSE, FALSE, nullptr));
+ base::UnguessableToken token =
+ registry_->RegisterDCOMPSurfaceHandle(std::move(handle));
+ {
+ base::AutoLock lock(tokens_lock);
+ tokens.push_back(token);
+ }
+ }
+ };
+
+ auto take_worker = [&]() {
+ threads_ready++;
+ while (!start_flag.load(std::memory_order_acquire)) {
+ std::this_thread::yield();
+ }
+
+ int taken = 0;
+ while (taken < kOpsPerThread) {
+ base::UnguessableToken token;
+ {
+ base::AutoLock lock(tokens_lock);
+ if (!tokens.empty()) {
+ token = tokens.back();
+ tokens.pop_back();
+ }
+ }
+ if (!token.is_empty()) {
+ base::win::ScopedHandle handle =
+ registry_->TakeDCOMPSurfaceHandle(token);
+ taken++;
+ } else {
+ std::this_thread::yield();
+ }
+ }
+ };
+
+ // With the barrier pattern, two threads are sufficient to trigger
+ // the race condition for TSAN.
+ std::thread t1(register_worker);
+ std::thread t2(take_worker);
+
+ // Wait until both threads are ready at the starting line.
+ while (threads_ready.load(std::memory_order_relaxed) < 2) {
+ std::this_thread::yield();
+ }
+
+ // Signal the staring flag to allow both threads to race from the initialized
+ // state.
+ start_flag.store(true, std::memory_order_release);
+
+ t1.join();
+ t2.join();
+}
+
+} // namespace gl

View File

@@ -0,0 +1,55 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Lynne Jiang <lyjiang@google.com>
Date: Fri, 20 Mar 2026 14:20:32 -0700
Subject: [webnn] Validate output channels are a multiple of groups in Conv2d.
Add a check in `ValidateConv2d` to ensure `output_channels % attributes.groups == 0`. This is a requirement for grouped convolutions. Add a unit test to cover this invalid case.
Bug: 493708165
Change-Id: Id83552a5fb2b95f84981f28ca7162331e17559cd
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7687895
Commit-Queue: Lynne Jiang <lyjiang@google.com>
Reviewed-by: Phillis Tang <phillis@chromium.org>
Reviewed-by: Reilly Grant <reillyg@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1602846}
diff --git a/services/webnn/public/cpp/graph_validation_utils.cc b/services/webnn/public/cpp/graph_validation_utils.cc
index 66464aee5d3f9699ccc060fcde5e263d1d07c8f6..e884cef6feb70fbe75e1a5c8fb4fa2712ed6b532 100644
--- a/services/webnn/public/cpp/graph_validation_utils.cc
+++ b/services/webnn/public/cpp/graph_validation_utils.cc
@@ -733,6 +733,10 @@ base::expected<OperandDescriptor, std::string> ValidateConv2dAndInferOutput(
"The groups must evenly divide the input channels to filter input "
"channels."));
}
+ if (output_channels % attributes.groups != 0) {
+ return base::unexpected(ErrorWithLabel(
+ label, "The groups must evenly divide the output channels."));
+ }
// Validate and calculate output sizes.
ASSIGN_OR_RETURN(
diff --git a/services/webnn/webnn_graph_impl_unittest.cc b/services/webnn/webnn_graph_impl_unittest.cc
index cab8eff2e739a5e3bc78a256c1b9599ca8d223f2..ece0d3950884fb024923b6598f5870baf1e5111a 100644
--- a/services/webnn/webnn_graph_impl_unittest.cc
+++ b/services/webnn/webnn_graph_impl_unittest.cc
@@ -1282,6 +1282,20 @@ TEST_F(WebNNGraphImplTest, Conv2dTest) {
.expected = false}
.Test(*this);
}
+ {
+ // Test invalid conv2d: output_channels is not a multiple of groups.
+ // output_channels (7) % groups (2) != 0.
+ Conv2dTester{.type = mojom::Conv2d::Kind::kDirect,
+ .input = {.type = OperandDataType::kFloat32,
+ .dimensions = {1, 4, 5, 5}},
+ .filter = {.type = OperandDataType::kFloat32,
+ .dimensions = {7, 2, 3, 3}},
+ .attributes = {.groups = 2},
+ .output = {.type = OperandDataType::kFloat32,
+ .dimensions = {1, 7, 3, 3}},
+ .expected = false}
+ .Test(*this);
+ }
{
// Test the invalid graph when the number of filter input channels
// doesn't match the result of input channels divided by groups

View File

@@ -0,0 +1,49 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Vasiliy Telezhnikov <vasilyt@chromium.org>
Date: Wed, 15 Apr 2026 09:02:46 -0700
Subject: Validate route_id for command buffers
Some route ids are reserved, we shouldn't allow command buffer
operations on them.
(cherry picked from commit 1880a4c3156b118953e2658f772afd666c0b3ed8)
Bug: 499891888
Fixed: 502436611
Change-Id: Id4c32103d8db70d9819112d4eb8d5c840d033300
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7747910
Reviewed-by: Kyle Charbonneau <kylechar@chromium.org>
Commit-Queue: Vasiliy Telezhnikov <vasilyt@chromium.org>
Cr-Original-Commit-Position: refs/heads/main@{#1613096}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7760833
Bot-Commit: rubber-stamper@appspot.gserviceaccount.com <rubber-stamper@appspot.gserviceaccount.com>
Cr-Commit-Position: refs/branch-heads/7680@{#3951}
Cr-Branched-From: 76b7d80e5cda23fe6537eed26d68c92e995c7f39-refs/heads/main@{#1582197}
diff --git a/gpu/ipc/service/gpu_channel.cc b/gpu/ipc/service/gpu_channel.cc
index 6918504a510d5a2a0aba3539156f72d53331d622..52aabbf3d5d52717e4c14e47c38cc3959194c973 100644
--- a/gpu/ipc/service/gpu_channel.cc
+++ b/gpu/ipc/service/gpu_channel.cc
@@ -976,6 +976,11 @@ void GpuChannel::CreateCommandBuffer(
return;
}
+ if (route_id <= static_cast<int32_t>(GpuChannelReservedRoutes::kMaxValue)) {
+ LOG(ERROR) << "ContextResult::kFatalFailure: using reserved route";
+ return;
+ }
+
int32_t stream_id = init_params->stream_id;
CommandBufferId command_buffer_id =
CommandBufferIdFromChannelAndRoute(client_id_, route_id);
@@ -1041,6 +1046,10 @@ void GpuChannel::DestroyCommandBuffer(int32_t route_id) {
return;
}
+ if (route_id <= static_cast<int32_t>(GpuChannelReservedRoutes::kMaxValue)) {
+ return;
+ }
+
std::unique_ptr<CommandBufferStub> stub;
auto it = stubs_.find(route_id);
if (it != stubs_.end()) {

View File

@@ -0,0 +1,211 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: junwei <junwei.fu@intel.com>
Date: Thu, 19 Mar 2026 04:36:10 -0700
Subject: WebNN: Use output size for TransposeConv SAME padding in TFLite
This CL aligns the TFLite backend's padding calculation for
convTranspose2d with the TFLite kernel implementation.
Previous implementation ignores WebNN convTranspose2d's non-zero output
padding which is not supported by TFLite SAME padding mode. However,
TFLite's TransposeConv kernel calculates 'SAME' padding by treating the
output size as the input to a regular convolution formula.
This CL also fixes an issue of the previous implementation that
incorrectly pads the input of transpose conv for explicit paddings
(crbug.com/491869941) by rejecting it. It should crop the output after
zero-padding (VALID) transpose conv instead. It will be implemented in a
separate CL.
Bug: 492668885, 491869941
Change-Id: Ibfbcd2bf9b80b6ab2b2f0fccf9596975537f9cc8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7677538
Reviewed-by: Hu, Ningxin <ningxin.hu@intel.com>
Commit-Queue: Fu, Junwei <junwei.fu@intel.com>
Reviewed-by: Reilly Grant <reillyg@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1601883}
diff --git a/services/webnn/tflite/graph_builder_tflite.cc b/services/webnn/tflite/graph_builder_tflite.cc
index 2754e597c7f6dd580369d348197d1ffa98722d6b..578627bf9b42885d170ec0b83b581e2b09984823 100644
--- a/services/webnn/tflite/graph_builder_tflite.cc
+++ b/services/webnn/tflite/graph_builder_tflite.cc
@@ -216,40 +216,50 @@ struct PaddingSizes {
// Helper to calculate the explicit padding for tflite::Padding_SAME mode with
// https://www.tensorflow.org/versions/r2.14/api_docs/python/tf/nn#notes_on_padding_2.
+// For transpose conv, caller should pass output size as input_size.
std::optional<PaddingSizes> CalculateExplicitPaddingForSamePaddingMode(
uint32_t input_size,
uint32_t filter_size,
uint32_t stride,
- uint32_t dilation,
- bool is_transposed_conv2d) {
- base::CheckedNumeric<uint32_t> checked_dilated_filter_size =
- (base::CheckedNumeric(filter_size) - 1) * dilation + 1;
- base::CheckedNumeric<uint32_t> checked_input_size = input_size;
- base::CheckedNumeric<uint32_t> checked_total_padding;
- if (is_transposed_conv2d) {
- // The checked_total_padding (beginningPadding + endingPadding) can be
- // calculated from the expression `outputSize = (inputSize - 1) * stride +
- // (filterSize - 1) * dilation + 1 - beginningPadding - endingPadding` that
- // is documented in the section of computing convtranspose output size:
- // https://www.w3.org/TR/webnn/#api-mlgraphbuilder-convtranspose2d
- checked_total_padding = (checked_input_size - 1) * stride +
- checked_dilated_filter_size -
- checked_input_size * stride;
- } else {
- auto checked_output_size = (checked_input_size + stride - 1) / stride;
- auto checked_needed_input_size =
- (checked_output_size - 1) * stride + checked_dilated_filter_size;
- if (!checked_needed_input_size.IsValid()) {
- return std::nullopt;
- }
- checked_total_padding = checked_needed_input_size.ValueOrDie() > input_size
- ? checked_needed_input_size - input_size
- : base::CheckedNumeric<uint32_t>(0);
+ uint32_t dilation) {
+ // The SAME padding mode in TFLite follows the formula:
+ // output_size = ceil(input_size / stride)
+ // total_padding = (output_size - 1) * stride + dilated_filter_size -
+ // input_size See:
+ // https://www.tensorflow.org/versions/r2.14/api_docs/python/tf/nn#notes_on_padding_2
+ auto checked_dilated_filter_size = base::CheckedNumeric<int32_t>(filter_size);
+ checked_dilated_filter_size -= 1;
+ checked_dilated_filter_size *= base::CheckedNumeric<int32_t>(dilation);
+ checked_dilated_filter_size += 1;
+
+ auto checked_input_size = base::CheckedNumeric<int32_t>(input_size);
+ auto checked_stride = base::CheckedNumeric<int32_t>(stride);
+ base::CheckedNumeric<int32_t> checked_output_size = checked_input_size;
+ checked_output_size += checked_stride;
+ checked_output_size -= 1;
+ checked_output_size /= checked_stride;
+
+ base::CheckedNumeric<int32_t> checked_needed_input_size = checked_output_size;
+ checked_needed_input_size -= 1;
+ checked_needed_input_size *= checked_stride;
+ checked_needed_input_size += checked_dilated_filter_size;
+ if (!checked_needed_input_size.IsValid()) {
+ return std::nullopt;
+ }
+ uint32_t needed_input_size;
+ if (!checked_needed_input_size.AssignIfValid(&needed_input_size)) {
+ return std::nullopt;
}
+ base::CheckedNumeric<uint32_t> checked_total_padding =
+ needed_input_size > input_size
+ ? base::CheckedNumeric<uint32_t>(needed_input_size) - input_size
+ : base::CheckedNumeric<uint32_t>(0);
// Same upper padding.
- auto checked_padding_begin = checked_total_padding / 2;
- auto checked_padding_end = (checked_total_padding + 1) / 2;
+ base::CheckedNumeric<uint32_t> checked_padding_begin =
+ checked_total_padding / 2;
+ base::CheckedNumeric<uint32_t> checked_padding_end =
+ (checked_total_padding + 1) / 2;
uint32_t padding_begin, padding_end;
if (!checked_padding_begin.AssignIfValid(&padding_begin) ||
!checked_padding_end.AssignIfValid(&padding_end)) {
@@ -274,19 +284,22 @@ base::expected<uint32_t, std::string> CalculatePaddingEndForCeilRoundingType(
uint32_t output_size,
uint32_t padding_begin) {
// Calculate the dilated filter sizes that are validated in graph validation.
- base::CheckedNumeric<uint32_t> checked_effective_filter_size = filter_size;
+ auto checked_effective_filter_size =
+ base::CheckedNumeric<int32_t>(filter_size);
checked_effective_filter_size -= 1;
- checked_effective_filter_size *= dilation;
+ checked_effective_filter_size *= base::CheckedNumeric<int32_t>(dilation);
checked_effective_filter_size += 1;
CHECK(checked_effective_filter_size.IsValid());
// Adjust ending padding to match the specified output.
- base::CheckedNumeric<uint32_t> checked_padding_end = output_size;
- checked_padding_end -= 1;
- checked_padding_end *= stride;
- checked_padding_end += checked_effective_filter_size;
- checked_padding_end -= input_size;
- checked_padding_end -= padding_begin;
+ auto checked_padding_end_int32 = base::CheckedNumeric<int32_t>(output_size);
+ checked_padding_end_int32 -= 1;
+ checked_padding_end_int32 *= base::CheckedNumeric<int32_t>(stride);
+ checked_padding_end_int32 += checked_effective_filter_size;
+ checked_padding_end_int32 -= base::CheckedNumeric<int32_t>(input_size);
+ checked_padding_end_int32 -= base::CheckedNumeric<int32_t>(padding_begin);
+
+ auto checked_padding_end = checked_padding_end_int32.Cast<uint32_t>();
// Check if the value is valid for rounding to uint32_t type.
if (!checked_padding_end.IsValid()) {
return base::unexpected("The padding end is too large.");
@@ -302,6 +315,7 @@ base::expected<TfLitePadding, std::string> GetTfLitePaddingMode(
const webnn::Size2d<uint32_t>& filter,
const mojom::Size2d& stride,
const mojom::Size2d& dilation,
+ const webnn::Size2d<uint32_t>& output,
bool is_transposed_conv2d) {
// WebNN explicit padding is in [beginning_height, ending_height,
// beginning_width, ending_width] sequence.
@@ -315,13 +329,18 @@ base::expected<TfLitePadding, std::string> GetTfLitePaddingMode(
// Convert the explicit padding to tflite same padding mode, The TFLite PAD
// operator need to be inserted if the calculated padding are not the same as
- // explicit padding.
+ // explicit padding for direct conv.
+ //
+ // In TFLite, TransposeConv's SAME padding is calculated based on the
+ // output size. See:
+ // https://source.chromium.org/chromium/chromium/src/+/main:third_party/litert/src/tflite/kernels/transpose_conv.cc;drc=7d88950ea445f5b671d18e64f9614aff397fde50;l=841
+ const uint32_t height_size =
+ is_transposed_conv2d ? output.height : input.height;
+ const uint32_t width_size = is_transposed_conv2d ? output.width : input.width;
const auto padding_height = CalculateExplicitPaddingForSamePaddingMode(
- input.height, filter.height, stride.height, dilation.height,
- is_transposed_conv2d);
+ height_size, filter.height, stride.height, dilation.height);
const auto padding_width = CalculateExplicitPaddingForSamePaddingMode(
- input.width, filter.width, stride.width, dilation.width,
- is_transposed_conv2d);
+ width_size, filter.width, stride.width, dilation.width);
if (!padding_height || !padding_width) {
return base::unexpected("Failed to calculate explicit padding.");
}
@@ -332,6 +351,15 @@ base::expected<TfLitePadding, std::string> GetTfLitePaddingMode(
return TfLitePadding{.mode = ::tflite::Padding_SAME};
}
+ // TFLite's TransposeConv SAME padding mode doesn't support output padding.
+ // Passing output size with non-zero output padding won't match and select
+ // TFLite SAME padding mode.
+ // TODO(crbug.com/493652470): Support explicit padding for transpose conv2d.
+ if (is_transposed_conv2d) {
+ return base::unexpected(
+ "Explicit padding is not supported for transpose conv2d.");
+ }
+
// The explicit padding are used to insert a TfLite PAD operator.
return TfLitePadding{.mode = ::tflite::Padding_VALID,
.paddings = explicit_padding};
@@ -380,7 +408,7 @@ base::expected<TfLitePadding, std::string> GetPool2dTfLitePaddingMode(
// Otherwise, a TFLite PAD operator will be inserted later using VALID
// padding.
return GetTfLitePaddingMode(padding2d, input, filter, stride, dilation,
- /*is_transposed_conv2d=*/false);
+ output, /*is_transposed_conv2d=*/false);
} else if (actual_output_height ==
base::ClampCeil<uint32_t>(calculated_output_sizes.height) &&
actual_output_width ==
@@ -4050,10 +4078,12 @@ auto GraphBuilderTflite::SerializeConv2d(const mojom::Conv2d& conv2d)
const auto& filter_shape = filter_operand.descriptor.shape();
const webnn::Size2d<uint32_t> filter_size2d = {.height = filter_shape[1],
.width = filter_shape[2]};
+ const webnn::Size2d<uint32_t> output_size2d = {.height = output_shape[1],
+ .width = output_shape[2]};
ASSIGN_OR_RETURN(
TfLitePadding padding_mode,
GetTfLitePaddingMode(*conv2d.padding, input_size2d, filter_size2d,
- *conv2d.strides, *conv2d.dilations,
+ *conv2d.strides, *conv2d.dilations, output_size2d,
conv2d.kind == mojom::Conv2d::Kind::kTransposed));
std::optional<FusedActivationOutputInfo> fused_activation =

View File

@@ -1,373 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <eugene@chromium.org>
Date: Mon, 13 Apr 2026 22:52:33 -0700
Subject: [M146] media: Zero-copy VP9 alpha decoding in VpxVideoDecoder
Original change's description:
> media: Zero-copy VP9 alpha decoding in VpxVideoDecoder
>
> Configures the VP9 alpha decoder to use `memory_pool_` for external
> frame buffers, eliminating the need for `libyuv::CopyPlane`.
>
> The `VideoFrame` now wraps the alpha data directly from the pool using
> a second destruction observer. `AllocateAlphaPlaneForFrameBuffer` and
> `alpha_data` tracking are removed from `FrameBufferPool`.
>
> Bug: 500066234
> Change-Id: I6e7cf13bcc8a5a1759acfd51961859c4c57fcbf2
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7737984
> Reviewed-by: Ted (Chromium) Meyer <tmathmeyer@chromium.org>
> Commit-Queue: Eugene Zemtsov <eugene@chromium.org>
> Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1611919}
(cherry picked from commit fc79e8cc2dfcc8f7ec8ee9cf0acf0993f32aec27)
Bug: 501314839,500066234
Change-Id: I6e7cf13bcc8a5a1759acfd51961859c4c57fcbf2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7757063
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Commit-Queue: Eugene Zemtsov <eugene@chromium.org>
Cr-Commit-Position: refs/branch-heads/7680@{#3937}
Cr-Branched-From: 76b7d80e5cda23fe6537eed26d68c92e995c7f39-refs/heads/main@{#1582197}
diff --git a/media/base/frame_buffer_pool.cc b/media/base/frame_buffer_pool.cc
index e90f07036baab4056398c93a03f8751bbfaa5d69..e2aa3a9243e3ce45b5087853eb2bd7d7dae7acfe 100644
--- a/media/base/frame_buffer_pool.cc
+++ b/media/base/frame_buffer_pool.cc
@@ -56,7 +56,6 @@ struct FrameBufferPool::FrameBuffer {
// Not using std::vector<uint8_t> as resize() calls take a really long time
// for large buffers.
BytesArray data;
- BytesArray alpha_data;
bool held_by_library = false;
// Needs to be a counter since a frame buffer might be used multiple times.
int held_by_frame = 0;
@@ -148,31 +147,6 @@ void FrameBufferPool::ReleaseFrameBuffer(void* fb_priv) {
}
}
-base::span<uint8_t> FrameBufferPool::AllocateAlphaPlaneForFrameBuffer(
- size_t min_size,
- void* fb_priv) {
- base::AutoLock lock(lock_);
- DCHECK(fb_priv);
-
- auto* frame_buffer = static_cast<FrameBuffer*>(fb_priv);
- DCHECK(IsUsedLocked(frame_buffer));
- if (frame_buffer->alpha_data.size() < min_size) {
- // Free the existing |alpha_data| first so that the memory can be reused,
- // if possible. Note that the new array is purposely not initialized.
- frame_buffer->alpha_data = {};
- uint8_t* data = nullptr;
- if (force_allocation_error_ ||
- !base::UncheckedMalloc(min_size, reinterpret_cast<void**>(&data)) ||
- !data) {
- return {};
- }
- // SAFETY: We have just allocated `min_size` of memory for `data`.
- frame_buffer->alpha_data =
- UNSAFE_BUFFERS(BytesArray::FromOwningPointer(data, min_size));
- }
- return frame_buffer->alpha_data;
-}
-
base::OnceClosure FrameBufferPool::CreateFrameCallback(void* fb_priv) {
base::AutoLock lock(lock_);
@@ -210,10 +184,9 @@ bool FrameBufferPool::OnMemoryDump(
size_t bytes_reserved = 0;
for (const auto& frame_buffer : frame_buffers_) {
if (IsUsedLocked(frame_buffer.get())) {
- bytes_used += frame_buffer->data.size() + frame_buffer->alpha_data.size();
+ bytes_used += frame_buffer->data.size();
}
- bytes_reserved +=
- frame_buffer->data.size() + frame_buffer->alpha_data.size();
+ bytes_reserved += frame_buffer->data.size();
}
memory_dump->AddScalar(base::trace_event::MemoryAllocatorDump::kNameSize,
diff --git a/media/base/frame_buffer_pool.h b/media/base/frame_buffer_pool.h
index ac839b8e8bfa00d2fea203be5248a56f04cecc71..2ccb01676b0e8e1e3ca1b3cb60f2883538f2f13c 100644
--- a/media/base/frame_buffer_pool.h
+++ b/media/base/frame_buffer_pool.h
@@ -48,11 +48,6 @@ class MEDIA_EXPORT FrameBufferPool
// Called when a frame buffer allocation is no longer needed.
void ReleaseFrameBuffer(void* fb_priv);
- // Allocates (or reuses) room for an alpha plane on a given frame buffer.
- // |fb_priv| must be a value previously returned by GetFrameBuffer().
- base::span<uint8_t> AllocateAlphaPlaneForFrameBuffer(size_t min_size,
- void* fb_priv);
-
// Generates a "no_longer_needed" closure that holds a reference to this pool;
// |fb_priv| must be a value previously returned by GetFrameBuffer(). The
// callback may be called on any thread.
diff --git a/media/base/frame_buffer_pool_unittest.cc b/media/base/frame_buffer_pool_unittest.cc
index a5b7bff2b8af3d2f9a531e894ec28e31e7823ac0..4cfdb1520cc18548fd91b2cca8b03a0124de944f 100644
--- a/media/base/frame_buffer_pool_unittest.cc
+++ b/media/base/frame_buffer_pool_unittest.cc
@@ -32,12 +32,6 @@ TEST(FrameBufferPool, BasicFunctionality) {
EXPECT_NE(buf1.data(), buf2.data());
std::ranges::fill(buf2, 0);
- auto alpha = pool->AllocateAlphaPlaneForFrameBuffer(kBufferSize, priv1);
- ASSERT_FALSE(alpha.empty());
- EXPECT_NE(alpha.data(), buf1.data());
- EXPECT_NE(alpha.data(), buf2.data());
- std::ranges::fill(alpha, 0);
-
EXPECT_EQ(2u, pool->get_pool_size_for_testing());
// Frames are not released immediately, so this should still show two frames.
@@ -52,7 +46,6 @@ TEST(FrameBufferPool, BasicFunctionality) {
EXPECT_EQ(1u, pool->get_pool_size_for_testing());
std::ranges::fill(buf1, 0);
- std::ranges::fill(alpha, 0);
// This will release all memory since we're in the shutdown state.
std::move(frame_release_cb).Run();
diff --git a/media/filters/vpx_video_decoder.cc b/media/filters/vpx_video_decoder.cc
index 0be38f7ee110a0084854c571784e9dd3c8144f51..32cd3c423f4f01aa4cbe21ae71bf149f26a1deee 100644
--- a/media/filters/vpx_video_decoder.cc
+++ b/media/filters/vpx_video_decoder.cc
@@ -269,7 +269,21 @@ bool VpxVideoDecoder::ConfigureDecoder(const VideoDecoderConfig& config) {
DCHECK(!vpx_codec_alpha_);
vpx_codec_alpha_ = InitializeVpxContext(config);
- return !!vpx_codec_alpha_;
+ if (!vpx_codec_alpha_) {
+ return false;
+ }
+
+ if (config.codec() == VideoCodec::kVP9) {
+ if (vpx_codec_set_frame_buffer_functions(
+ vpx_codec_alpha_.get(), &GetVP9FrameBuffer, &ReleaseVP9FrameBuffer,
+ memory_pool_.get())) {
+ DLOG(ERROR) << "Failed to configure external buffers for alpha. "
+ << vpx_codec_error(vpx_codec_alpha_.get());
+ return false;
+ }
+ }
+
+ return true;
}
void VpxVideoDecoder::CloseDecoder() {
@@ -576,20 +590,13 @@ bool VpxVideoDecoder::CopyVpxImageToVideoFrame(
if (memory_pool_) {
DCHECK_EQ(VideoCodec::kVP9, config_.codec());
if (vpx_image_alpha) {
+ CHECK_GT(vpx_image_alpha->stride[VPX_PLANE_Y], 0);
size_t alpha_plane_size =
vpx_image_alpha->stride[VPX_PLANE_Y] * vpx_image_alpha->d_h;
- auto alpha_plane = memory_pool_->AllocateAlphaPlaneForFrameBuffer(
- alpha_plane_size, vpx_image->fb_priv);
- if (alpha_plane.empty()) {
- error_status_ = DecoderStatus::Codes::kOutOfMemory;
- // In case of OOM, abort copy.
- return false;
- }
- libyuv::CopyPlane(vpx_image_alpha->planes[VPX_PLANE_Y],
- vpx_image_alpha->stride[VPX_PLANE_Y],
- alpha_plane.data(),
- vpx_image_alpha->stride[VPX_PLANE_Y],
- vpx_image_alpha->d_w, vpx_image_alpha->d_h);
+ // SAFETY: libvpx guarantees that the Y plane has at least `stride * d_h`
+ // bytes available.
+ auto alpha_plane = UNSAFE_BUFFERS(base::span<uint8_t>(
+ vpx_image_alpha->planes[VPX_PLANE_Y], alpha_plane_size));
*video_frame = VideoFrame::WrapExternalYuvaData(
codec_format, coded_size, gfx::Rect(visible_size), natural_size,
vpx_image->stride[VPX_PLANE_Y], vpx_image->stride[VPX_PLANE_U],
@@ -605,8 +612,14 @@ bool VpxVideoDecoder::CopyVpxImageToVideoFrame(
if (!(*video_frame))
return false;
- video_frame->get()->AddDestructionObserver(
- memory_pool_->CreateFrameCallback(vpx_image->fb_priv));
+ (*video_frame)
+ ->AddDestructionObserver(
+ memory_pool_->CreateFrameCallback(vpx_image->fb_priv));
+ if (vpx_image_alpha) {
+ (*video_frame)
+ ->AddDestructionObserver(
+ memory_pool_->CreateFrameCallback(vpx_image_alpha->fb_priv));
+ }
return true;
}
diff --git a/media/filters/vpx_video_decoder.h b/media/filters/vpx_video_decoder.h
index 7bcba319954ed43175e42c2dc1b991c5b6129138..2ab3767680ee408215bf2debb6f85c033f45af68 100644
--- a/media/filters/vpx_video_decoder.h
+++ b/media/filters/vpx_video_decoder.h
@@ -104,8 +104,8 @@ class MEDIA_EXPORT VpxVideoDecoder : public OffloadableVideoDecoder {
std::unique_ptr<vpx_codec_ctx> vpx_codec_;
std::unique_ptr<vpx_codec_ctx> vpx_codec_alpha_;
- // |memory_pool_| is a single-threaded memory pool used for VP9 decoding
- // with no alpha. |frame_pool_| is used for all other cases.
+ // |memory_pool_| is a thread-safe memory pool used for zero-copy VP9 decoding
+ // (both with and without alpha). |frame_pool_| is used for VP8.
scoped_refptr<FrameBufferPool> memory_pool_;
VideoFramePool frame_pool_;
diff --git a/media/filters/vpx_video_decoder_unittest.cc b/media/filters/vpx_video_decoder_unittest.cc
index c7f6d13bd825425230b63d87c13466e49f3c3c59..5203645bc8ec89dd93827fc0cbebb92e803faac1 100644
--- a/media/filters/vpx_video_decoder_unittest.cc
+++ b/media/filters/vpx_video_decoder_unittest.cc
@@ -176,6 +176,28 @@ class VpxVideoDecoderTest : public testing::Test {
output_frames_.push_back(std::move(frame));
}
+ // Extracts the compressed video data from the AVPacket and also checks for
+ // side data containing an alpha channel. If found, it copies the alpha data
+ // into the DecoderBuffer's side data. This is necessary because FFmpeg
+ // demuxes alpha channel data as side data associated with the video packet.
+ static scoped_refptr<DecoderBuffer> CreateBufferWithAlphaFromPacket(
+ const AVPacket* packet) {
+ auto buffer = DecoderBuffer::CopyFrom(AVPacketData(*packet));
+ size_t side_data_size = 0;
+ uint8_t* side_data_ptr = av_packet_get_side_data(
+ packet, AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL, &side_data_size);
+ if (side_data_size > 8) {
+ // SAFETY: The best we can do here is trust the size reported by ffmpeg.
+ auto side_data =
+ UNSAFE_BUFFERS(base::span(side_data_ptr, side_data_size));
+ if (base::U64FromBigEndian(side_data.first<8u>()) == 1) {
+ buffer->WritableSideData().alpha_data =
+ base::HeapArray<uint8_t>::CopiedFrom(side_data.subspan(8u));
+ }
+ }
+ return buffer;
+ }
+
MOCK_METHOD1(DecodeDone, void(DecoderStatus));
base::test::TaskEnvironment task_env_;
@@ -293,6 +315,68 @@ TEST_F(VpxVideoDecoderTest, SimpleFrameReuse) {
EXPECT_EQ(old_y_data, output_frames_.back()->data(VideoFrame::Plane::kY));
}
+TEST_F(VpxVideoDecoderTest, SimpleAlphaFrameReuse) {
+ VideoDecoderConfig config = TestVideoConfig::Normal(VideoCodec::kVP9);
+ config.Initialize(
+ config.codec(), config.profile(),
+ VideoDecoderConfig::AlphaMode::kHasAlpha, config.color_space_info(),
+ config.video_transformation(), config.coded_size(), config.visible_rect(),
+ config.natural_size(), config.extra_data(), config.encryption_scheme());
+ InitializeWithConfig(config);
+ scoped_refptr<DecoderBuffer> alpha_frame = ReadTestDataFile("bear-vp9a.webm");
+
+ // Read frames from the webm file.
+ InMemoryUrlProtocol protocol(*alpha_frame, false);
+ FFmpegGlue glue(&protocol);
+ ASSERT_TRUE(glue.OpenContext());
+
+ auto packet = ScopedAVPacket::Allocate();
+
+ // Decode first frame
+ ASSERT_GE(av_read_frame(glue.format_context(), packet.get()), 0);
+ auto buffer = CreateBufferWithAlphaFromPacket(packet.get());
+ Decode(buffer);
+ av_packet_unref(packet.get());
+
+ ASSERT_EQ(1u, output_frames_.size());
+ scoped_refptr<VideoFrame> frame = std::move(output_frames_.front());
+ EXPECT_EQ(PIXEL_FORMAT_I420A, frame->format());
+ const uint8_t* old_y_data = frame->data(VideoFrame::Plane::kY);
+ const uint8_t* old_a_data = frame->data(VideoFrame::Plane::kA);
+ output_frames_.pop_back();
+
+ // Clear frame reference to return the frame to the pool.
+ frame = nullptr;
+
+ // Decode second frame.
+ Decode(buffer);
+ const uint8_t* mid_y_data =
+ output_frames_.front()->data(VideoFrame::Plane::kY);
+ const uint8_t* mid_a_data =
+ output_frames_.front()->data(VideoFrame::Plane::kA);
+ output_frames_.clear();
+
+ // Issuing another decode should reuse buffers from the pool.
+ Decode(buffer);
+
+ ASSERT_EQ(1u, output_frames_.size());
+ const uint8_t* new_y_data =
+ output_frames_.back()->data(VideoFrame::Plane::kY);
+ const uint8_t* new_a_data =
+ output_frames_.back()->data(VideoFrame::Plane::kA);
+
+ // The pool is shared, so buffers might be reused in a different order (e.g. Y
+ // might get the buffer previously used for A). Because libvpx allocates the
+ // new frame before releasing the old reference frame, we need to check across
+ // all previously allocated buffers.
+ bool reused_y = new_y_data == old_y_data || new_y_data == old_a_data ||
+ new_y_data == mid_y_data || new_y_data == mid_a_data;
+ bool reused_a = new_a_data == old_y_data || new_a_data == old_a_data ||
+ new_a_data == mid_y_data || new_a_data == mid_a_data;
+ EXPECT_TRUE(reused_y);
+ EXPECT_TRUE(reused_a);
+}
+
TEST_F(VpxVideoDecoderTest, SimpleFormatChange) {
scoped_refptr<DecoderBuffer> large_frame =
ReadTestDataFile("vp9-I-frame-1280x720");
@@ -312,9 +396,41 @@ TEST_F(VpxVideoDecoderTest, FrameValidAfterPoolDestruction) {
// Write to the Y plane. The memory tools should detect a
// use-after-free if the storage was actually removed by pool destruction.
- memset(output_frames_.front()->writable_data(VideoFrame::Plane::kY), 0xff,
- output_frames_.front()->rows(VideoFrame::Plane::kY) *
- output_frames_.front()->stride(VideoFrame::Plane::kY));
+ std::ranges::fill(
+ output_frames_.front()->writable_span(VideoFrame::Plane::kY), 0xff);
+}
+
+TEST_F(VpxVideoDecoderTest, AlphaFrameValidAfterPoolDestruction) {
+ VideoDecoderConfig config = TestVideoConfig::Normal(VideoCodec::kVP9);
+ config.Initialize(
+ config.codec(), config.profile(),
+ VideoDecoderConfig::AlphaMode::kHasAlpha, config.color_space_info(),
+ config.video_transformation(), config.coded_size(), config.visible_rect(),
+ config.natural_size(), config.extra_data(), config.encryption_scheme());
+ InitializeWithConfig(config);
+ scoped_refptr<DecoderBuffer> alpha_frame = ReadTestDataFile("bear-vp9a.webm");
+
+ InMemoryUrlProtocol protocol(*alpha_frame, false);
+ FFmpegGlue glue(&protocol);
+ ASSERT_TRUE(glue.OpenContext());
+
+ auto packet = ScopedAVPacket::Allocate();
+ ASSERT_GE(av_read_frame(glue.format_context(), packet.get()), 0);
+ auto buffer = CreateBufferWithAlphaFromPacket(packet.get());
+ Decode(std::move(buffer));
+ av_packet_unref(packet.get());
+
+ ASSERT_EQ(1u, output_frames_.size());
+ EXPECT_EQ(PIXEL_FORMAT_I420A, output_frames_.front()->format());
+
+ Destroy();
+
+ // Write to the Y and A planes. The memory tools should detect a
+ // use-after-free if the storage was actually removed by pool destruction.
+ std::ranges::fill(
+ output_frames_.front()->writable_span(VideoFrame::Plane::kY), 0xff);
+ std::ranges::fill(
+ output_frames_.front()->writable_span(VideoFrame::Plane::kA), 0xff);
}
// The test stream uses profile 2, which needs high bit depth support in libvpx.
@@ -362,8 +478,7 @@ TEST_F(VpxVideoDecoderTest, MemoryPoolAllowsMultipleDisplay) {
Destroy();
// ASAN will be very unhappy with this line if the above is incorrect.
- memset(last_frame->writable_data(VideoFrame::Plane::kY), 0,
- last_frame->row_bytes(VideoFrame::Plane::kY));
+ std::ranges::fill(last_frame->writable_span(VideoFrame::Plane::kY), 0);
}
#endif // !defined(LIBVPX_NO_HIGH_BIT_DEPTH) && !defined(ARCH_CPU_ARM_FAMILY)

View File

@@ -0,0 +1,197 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Dale Curtis <dalecurtis@chromium.org>
Date: Tue, 17 Mar 2026 17:33:03 -0700
Subject: Ensure AudioRendererMixer holds lock during sink switch
This guards `switch_output_device_in_progress_` with a lock that
can be held during the final phase of a setSinkId() operation
within the AudioRendererMixerInput. It ensures that if Stop()
is called, we don't incorrectly reconnect the new sink, and if
a device changes is in flight, that we stall the Stop() call.
R=tguilbert
Fixed: 492218537
Change-Id: I9ec6efb9678762a22b1b1c8a2f8918771c264678
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7673253
Reviewed-by: Thomas Guilbert <tguilbert@chromium.org>
Commit-Queue: Dale Curtis <dalecurtis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1600910}
diff --git a/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.cc b/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.cc
index a3160d119f3e1218f1b5cac26c4cebeeb096017f..ce6b35edb32b619f88e5d4d003ce136da0458373 100644
--- a/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.cc
+++ b/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.cc
@@ -96,6 +96,17 @@ void AudioRendererMixerInput::Start() {
}
void AudioRendererMixerInput::Stop() {
+ {
+ // Prevents race conditions when Stop() is called during a device change.
+ base::AutoLock auto_lock(device_change_lock_);
+ if (switch_output_device_in_progress_) {
+ switch_output_device_in_progress_ = false;
+ }
+ }
+ StopInternal();
+}
+
+void AudioRendererMixerInput::StopInternal() {
// Stop() may be called at any time, if Pause() hasn't been called we need to
// remove our mixer input before shutdown.
Pause();
@@ -154,10 +165,13 @@ void AudioRendererMixerInput::GetOutputDeviceInfoAsync(
return;
}
- if (switch_output_device_in_progress_) {
- DCHECK(!godia_in_progress_);
- pending_device_info_cb_ = std::move(info_cb);
- return;
+ {
+ base::AutoLock auto_lock(device_change_lock_);
+ if (switch_output_device_in_progress_) {
+ DCHECK(!godia_in_progress_);
+ pending_device_info_cb_ = std::move(info_cb);
+ return;
+ }
}
godia_in_progress_ = true;
@@ -192,7 +206,10 @@ void AudioRendererMixerInput::SwitchOutputDevice(
media::OutputDeviceStatusCB callback) {
// If a GODIA() call is in progress, defer until it's complete.
if (godia_in_progress_) {
- DCHECK(!switch_output_device_in_progress_);
+ {
+ base::AutoLock auto_lock(device_change_lock_);
+ DCHECK(!switch_output_device_in_progress_);
+ }
// Abort any previous device switch which may be pending.
if (pending_switch_cb_) {
@@ -214,7 +231,10 @@ void AudioRendererMixerInput::SwitchOutputDevice(
return;
}
- switch_output_device_in_progress_ = true;
+ {
+ base::AutoLock auto_lock(device_change_lock_);
+ switch_output_device_in_progress_ = true;
+ }
// Request a new sink using the new device id. This process may fail, so to
// avoid interrupting working audio, don't set any class variables until we
@@ -307,45 +327,48 @@ void AudioRendererMixerInput::OnDeviceSwitchReady(
media::OutputDeviceStatusCB switch_cb,
scoped_refptr<media::AudioRendererSink> sink,
media::OutputDeviceInfo device_info) {
- DCHECK(switch_output_device_in_progress_);
- switch_output_device_in_progress_ = false;
-
- if (device_info.device_status() != media::OUTPUT_DEVICE_STATUS_OK) {
- sink->Stop();
- std::move(switch_cb).Run(device_info.device_status());
-
- // Start any pending device info request.
- if (pending_device_info_cb_) {
- GetOutputDeviceInfoAsync(std::move(pending_device_info_cb_));
- }
+ auto return_status = device_info.device_status();
- return;
- }
-
- const bool has_mixer = !!mixer_;
- const bool is_playing = playing_;
-
- // This may occur if Start() hasn't yet been called.
- if (sink_) {
- sink_->Stop();
- }
+ {
+ base::AutoLock auto_lock(device_change_lock_);
+
+ if (device_info.device_status() != media::OUTPUT_DEVICE_STATUS_OK) {
+ // Case: Device change failed.
+ sink->Stop();
+ } else if (!switch_output_device_in_progress_) {
+ // Case: Stop() called during device change.
+ sink->Stop();
+ return_status = media::OUTPUT_DEVICE_STATUS_ERROR_INTERNAL;
+ } else {
+ // Case: Device change succeeded, connect to new sink.
+ const bool has_mixer = !!mixer_;
+ const bool is_playing = playing_;
+
+ // This may occur if Start() hasn't yet been called.
+ if (sink_) {
+ sink_->Stop();
+ }
- sink_ = std::move(sink);
- device_info_ = device_info;
- device_id_ = device_info.device_id();
+ sink_ = std::move(sink);
+ device_info_ = device_info;
+ device_id_ = device_info.device_id();
- auto callback = callback_;
- Stop();
- callback_ = callback;
+ auto callback = callback_;
+ StopInternal();
+ callback_ = callback;
- if (has_mixer) {
- Start();
- if (is_playing) {
- Play();
+ if (has_mixer) {
+ Start();
+ if (is_playing) {
+ Play();
+ }
+ }
}
+
+ switch_output_device_in_progress_ = false;
}
- std::move(switch_cb).Run(device_info.device_status());
+ std::move(switch_cb).Run(return_status);
// Start any pending device info request.
if (pending_device_info_cb_) {
diff --git a/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.h b/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.h
index e13df618d5cc86221a12d060d892474e2cbc8c49..f511187bfd7f164c0c5d7c10a894a3e81c0a7fbc 100644
--- a/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.h
+++ b/third_party/blink/renderer/modules/media/audio/audio_renderer_mixer_input.h
@@ -80,6 +80,7 @@ class BLINK_MODULES_EXPORT AudioRendererMixerInput
void OnRenderError();
private:
+ void StopInternal();
~AudioRendererMixerInput() override;
friend class AudioRendererMixerInputTest;
@@ -118,6 +119,9 @@ class BLINK_MODULES_EXPORT AudioRendererMixerInput
scoped_refptr<media::AudioRendererSink> sink,
media::OutputDeviceInfo device_info);
+ // Prevents race conditions when Stop() is called during a device change.
+ base::Lock device_change_lock_;
+
// AudioParameters received during Initialize().
media::AudioParameters params_;
@@ -143,7 +147,8 @@ class BLINK_MODULES_EXPORT AudioRendererMixerInput
// exclusive when executing; these flags indicate whether one or the other is
// in progress. Each method will use the other method's to defer its action.
bool godia_in_progress_ = false;
- bool switch_output_device_in_progress_ = false;
+ bool switch_output_device_in_progress_ GUARDED_BY(device_change_lock_) =
+ false;
// Set by GetOutputDeviceInfoAsync() if a SwitchOutputDevice() call is in
// progress. GetOutputDeviceInfoAsync() will be invoked again with this value

View File

@@ -0,0 +1,50 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: JunHo Seo <junho0924.seo@lge.com>
Date: Wed, 25 Feb 2026 19:59:16 -0800
Subject: Fix DCHECK failure when starting heap profiler for renderer.
Backports https://crrev.com/c/7603976.
Heap profiling for the renderer is currently started in
OnRenderProcessHostCreated(). However, at that point, base::Process::Pid
may not yet be valid, which can trigger a DCHECK(IsValid()) failure.
In addition, even if the DCHECK does not fire, the PID might still be
zero, causing renderer profiling data to be aggregated incorrectly.
To address this issue, this CL moves the heap profiler startup to
OnRenderProcessLaunched().
Bug: N/A
Change-Id: If1ba076dcf59d84b875a0b09544df9fde0dee83a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7603976
Commit-Queue: JunHo Seo <junho0924.seo@lge.com>
Reviewed-by: Joe Mason <joenotcharles@google.com>
Reviewed-by: Rohit Rao <rohitrao@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1590612}
diff --git a/components/heap_profiling/multi_process/client_connection_manager.cc b/components/heap_profiling/multi_process/client_connection_manager.cc
index 6a4c109d8f31a4658e60f49d15c4a62c9d272775..93c3b4da047e05ca622a4d759effeb0709c619a9 100644
--- a/components/heap_profiling/multi_process/client_connection_manager.cc
+++ b/components/heap_profiling/multi_process/client_connection_manager.cc
@@ -260,7 +260,7 @@ void ClientConnectionManager::StartProfilingNonRendererChild(
std::move(started_profiling_closure)));
}
-void ClientConnectionManager::OnRenderProcessHostCreated(
+void ClientConnectionManager::OnRenderProcessLaunched(
content::RenderProcessHost* host) {
if (ShouldProfileNewRenderer(host)) {
StartProfilingRenderer(host, base::DoNothing());
diff --git a/components/heap_profiling/multi_process/client_connection_manager.h b/components/heap_profiling/multi_process/client_connection_manager.h
index 80f34e3dbbcbd09645bb1c2b35b91cdaa74226ba..d298bbdc75ba5a3c857e4c04a6b65920f9161cea 100644
--- a/components/heap_profiling/multi_process/client_connection_manager.h
+++ b/components/heap_profiling/multi_process/client_connection_manager.h
@@ -100,7 +100,7 @@ class ClientConnectionManager
started_profiling_closure);
// content::RenderProcessHostCreationObserver
- void OnRenderProcessHostCreated(content::RenderProcessHost* host) override;
+ void OnRenderProcessLaunched(content::RenderProcessHost* host) override;
// RenderProcessHostObserver:
// RenderProcessHostDestroyed() corresponds to death of an underlying

View File

@@ -0,0 +1,30 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Noah Gregory <noahmgregory@gmail.com>
Date: Thu, 23 Apr 2026 11:11:44 -0400
Subject: fix: make macOS text replacement work on `contenteditable`
Text-editor libraries like `lexical` don't use `input` elements,
but instead use `contenteditable` elements. macOS text replacement
is currently bugged on these elements. This patch fixes that.
1. Backspace now rejects the replacement instead of accepting it.
2. Space now adds a space after accepting the replacement.
diff --git a/content/app_shim_remote_cocoa/render_widget_host_view_cocoa.mm b/content/app_shim_remote_cocoa/render_widget_host_view_cocoa.mm
index de24209bbd3cd4a530c6f32990a0f93a182abfc0..ef028e82621ef681aa6f1f543e87d136dcc1fe64 100644
--- a/content/app_shim_remote_cocoa/render_widget_host_view_cocoa.mm
+++ b/content/app_shim_remote_cocoa/render_widget_host_view_cocoa.mm
@@ -519,6 +519,13 @@ - (void)didAcceptReplacementString:(NSString*)acceptedString
if (acceptedString == nil)
return;
+ if (changeNumber != _availableTextChangeCounter) {
+ if (!_textSelectionRange.is_empty() ||
+ _textSelectionRange.start() <= NSMaxRange(correction.range)) {
+ return;
+ }
+ }
+
NSRange availableTextRange =
NSMakeRange(_availableTextOffset, _availableText.length());

View File

@@ -1209,7 +1209,7 @@ index a1068589ad844518038ee7bc15a3de9bc5cba525..1ff781c49f086ec8015c7d3c44567dbe
} // namespace content
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index d368b2481156bb79c6e74c8b09a828eb2fa2d44c..07cbf495717714d71d977a8820e08050c3062526 100644
index fa04ab07ac1a5a0b0ff2dec4dba6cb2d1a0ab2d0..f5d72a89c7229bf8e897c90660feca482ac82594 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -700,6 +700,7 @@ static_library("test_support") {
@@ -1237,7 +1237,7 @@ index d368b2481156bb79c6e74c8b09a828eb2fa2d44c..07cbf495717714d71d977a8820e08050
]
if (!(is_chromeos && target_cpu == "arm64" && current_cpu == "arm")) {
@@ -3412,6 +3416,7 @@ test("content_unittests") {
@@ -3413,6 +3417,7 @@ test("content_unittests") {
"//ui/shell_dialogs",
"//ui/webui:test_support",
"//url",

View File

@@ -0,0 +1,22 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: reito <reito@chromium.org>
Date: Wed, 29 Oct 2025 00:50:03 +0800
Subject: patch: osr control screen info
We need to override GetNewScreenInfosForUpdate to ensure the screen info
is updated correctly, instead of overriding GetScreenInfo which seems not
working.
diff --git a/content/browser/renderer_host/render_widget_host_view_base.h b/content/browser/renderer_host/render_widget_host_view_base.h
index 1a18bdda39f76cfae36adc0ffde136e788a98262..1062bada30908399f5429b51031e245f4d010f84 100644
--- a/content/browser/renderer_host/render_widget_host_view_base.h
+++ b/content/browser/renderer_host/render_widget_host_view_base.h
@@ -680,7 +680,7 @@ class CONTENT_EXPORT RenderWidgetHostViewBase
// Generates the most current set of ScreenInfos from the current set of
// displays in the system for use in UpdateScreenInfo.
- display::ScreenInfos GetNewScreenInfosForUpdate();
+ virtual display::ScreenInfos GetNewScreenInfosForUpdate();
// Called when display properties that need to be synchronized with the
// renderer process changes. This method is called before notifying

View File

@@ -15,6 +15,5 @@
{ "patch_dir": "src/electron/patches/sqlite", "repo": "src/third_party/sqlite/src" },
{ "patch_dir": "src/electron/patches/angle", "repo": "src/third_party/angle" },
{ "patch_dir": "src/electron/patches/skia", "repo": "src/third_party/skia" },
{ "patch_dir": "src/electron/patches/pdfium", "repo": "src/third_party/pdfium" },
{ "patch_dir": "src/electron/patches/libaom", "repo": "src/third_party/libaom/source/libaom" }
]

View File

@@ -1,4 +1,2 @@
cherry-pick-4369bd1258dc.patch
cherry-pick-a047955845e5.patch
cherry-pick-c61e9586156f.patch
cherry-pick-395efd18d8ef.patch

View File

@@ -1,24 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: James Zern <jzern@google.com>
Date: Fri, 27 Mar 2026 10:56:13 -0700
Subject: av1_nonrd_pick_inter_mode_sb: add missing ref_frame_flags check
Before calling `set_block_source_sad()` ensure `LAST_FRAME` is
available. Fixes a crash that may present as a use after free (UAF).
Bug: 495477995, 495996858
Change-Id: I61452ce412fb9071c3370b4350ed8878013a8355
(cherry picked from commit 4369bd1258dc99fa759916d9aba6509cdda9d877)
diff --git a/av1/encoder/nonrd_pickmode.c b/av1/encoder/nonrd_pickmode.c
index f2010062323b0ff4a1236ef63516d9b2d8f3007a..0f2a1c780a56a51f69bba8893fea9d9ad98b85a3 100644
--- a/av1/encoder/nonrd_pickmode.c
+++ b/av1/encoder/nonrd_pickmode.c
@@ -3440,6 +3440,7 @@ void av1_nonrd_pick_inter_mode_sb(AV1_COMP *cpi, TileDataEnc *tile_data,
!x->force_zeromv_skip_for_blk &&
x->content_state_sb.source_sad_nonrd != kZeroSad &&
x->source_variance == 0 && bsize < cm->seq_params->sb_size &&
+ (cpi->ref_frame_flags & AOM_LAST_FLAG) &&
search_state.yv12_mb[LAST_FRAME][0].width == cm->width &&
search_state.yv12_mb[LAST_FRAME][0].height == cm->height) {
set_block_source_sad(cpi, x, bsize, &search_state.yv12_mb[LAST_FRAME][0]);

View File

@@ -1,187 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Marco Paniconi <marpan@google.com>
Date: Sun, 29 Mar 2026 20:27:20 -0700
Subject: Set force_mv_inter_layer earlier in skip_inter_mode
For nonrd_pickmode: move the setting of
force_mv_inter_layer earlier in the
skip_inter_mode_nonrd(), to make sure it always
get set (in case of false return in that function).
Thie prevents the usage of a scaled_ref in pickmode
(combined_motion search) when it has actually not been
set/scaled in av1_scale_references (before encoding).
Fixes a crash for use after free (UAF), reported
in the issues below.
Added svc unittest to generate the issue. Also added
assert check for scaled_ref in combined_motion_search.
Bug: 495477995, 495996858
Change-Id: I578d19156d97a50546edc9422bc3581566f1236e
(cherry picked from commit a047955845e50e43786d51cdefcfc9e87804ed61)
diff --git a/av1/encoder/nonrd_pickmode.c b/av1/encoder/nonrd_pickmode.c
index 0f2a1c780a56a51f69bba8893fea9d9ad98b85a3..942b8ab23a2d448877c8801940fee4d0baae9aef 100644
--- a/av1/encoder/nonrd_pickmode.c
+++ b/av1/encoder/nonrd_pickmode.c
@@ -192,7 +192,7 @@ static int combined_motion_search(AV1_COMP *cpi, MACROBLOCK *x,
int *rate_mv, int64_t best_rd_sofar,
int use_base_mv) {
MACROBLOCKD *xd = &x->e_mbd;
- const AV1_COMMON *cm = &cpi->common;
+ AV1_COMMON *cm = &cpi->common;
const SPEED_FEATURES *sf = &cpi->sf;
MB_MODE_INFO *mi = xd->mi[0];
int step_param = (sf->rt_sf.fullpel_search_step_param)
@@ -207,6 +207,14 @@ static int combined_motion_search(AV1_COMP *cpi, MACROBLOCK *x,
int cost_list[5];
int search_subpel = 1;
+ if (av1_is_scaled(get_ref_scale_factors(cm, ref))) {
+ const YV12_BUFFER_CONFIG *scaled_ref = av1_get_scaled_ref_frame(cpi, ref);
+ (void)scaled_ref;
+ assert(scaled_ref != NULL);
+ assert(scaled_ref->y_crop_width == cm->width &&
+ scaled_ref->y_crop_height == cm->height);
+ }
+
start_mv = get_fullmv_from_mv(&ref_mv);
if (!use_base_mv)
@@ -2490,6 +2498,23 @@ static AOM_FORCE_INLINE bool skip_inter_mode_nonrd(
(*this_mode != GLOBALMV || *ref_frame != LAST_FRAME))
return true;
+ *force_mv_inter_layer = 0;
+ if (cpi->ppi->use_svc && svc->spatial_layer_id > 0 &&
+ ((*ref_frame == LAST_FRAME && svc->skip_mvsearch_last) ||
+ (*ref_frame == GOLDEN_FRAME && svc->skip_mvsearch_gf) ||
+ (*ref_frame == ALTREF_FRAME && svc->skip_mvsearch_altref))) {
+ // Only test mode if NEARESTMV/NEARMV is (svc_mv.mv.col, svc_mv.mv.row),
+ // otherwise set NEWMV to (svc_mv.mv.col, svc_mv.mv.row).
+ // Skip newmv and filter search.
+ *force_mv_inter_layer = 1;
+ if (*this_mode == NEWMV) {
+ search_state->frame_mv[*this_mode][*ref_frame] = svc_mv;
+ } else if (search_state->frame_mv[*this_mode][*ref_frame].as_int !=
+ svc_mv.as_int) {
+ return true;
+ }
+ }
+
// If the segment reference frame feature is enabled then do nothing if the
// current ref frame is not allowed.
if (segfeature_active(seg, segment_id, SEG_LVL_REF_FRAME)) {
@@ -2565,23 +2590,6 @@ static AOM_FORCE_INLINE bool skip_inter_mode_nonrd(
return true;
}
- *force_mv_inter_layer = 0;
- if (cpi->ppi->use_svc && svc->spatial_layer_id > 0 &&
- ((*ref_frame == LAST_FRAME && svc->skip_mvsearch_last) ||
- (*ref_frame == GOLDEN_FRAME && svc->skip_mvsearch_gf) ||
- (*ref_frame == ALTREF_FRAME && svc->skip_mvsearch_altref))) {
- // Only test mode if NEARESTMV/NEARMV is (svc_mv.mv.col, svc_mv.mv.row),
- // otherwise set NEWMV to (svc_mv.mv.col, svc_mv.mv.row).
- // Skip newmv and filter search.
- *force_mv_inter_layer = 1;
- if (*this_mode == NEWMV) {
- search_state->frame_mv[*this_mode][*ref_frame] = svc_mv;
- } else if (search_state->frame_mv[*this_mode][*ref_frame].as_int !=
- svc_mv.as_int) {
- return true;
- }
- }
-
// For screen content: skip mode testing based on source_sad.
if (cpi->oxcf.tune_cfg.content == AOM_CONTENT_SCREEN &&
!x->force_zeromv_skip_for_blk) {
diff --git a/test/svc_datarate_test.cc b/test/svc_datarate_test.cc
index 0df678212acb0519aa4420ae57186840e12c682c..2f68ba7a214932b284a6eacbe1a9b5b474b6c659 100644
--- a/test/svc_datarate_test.cc
+++ b/test/svc_datarate_test.cc
@@ -247,6 +247,7 @@ class DatarateTestSVC
external_resize_pattern_ = 0;
dynamic_tl_ = false;
dynamic_scale_factors_ = false;
+ disable_last_ref_ = false;
}
void PreEncodeFrameHook(::libaom_test::VideoSource *video,
@@ -302,7 +303,7 @@ class DatarateTestSVC
spatial_layer_id, multi_ref_, comp_pred_,
(video->frame() % cfg_.kf_max_dist) == 0, dynamic_enable_disable_mode_,
rps_mode_, rps_recovery_frame_, simulcast_mode_, use_last_as_scaled_,
- use_last_as_scaled_single_ref_);
+ use_last_as_scaled_single_ref_, disable_last_ref_);
if (intra_only_ == 1 && frame_sync_ > 0) {
// Set an Intra-only frame on SL0 at frame_sync_.
// In order to allow decoding to start on SL0 in mid-sequence we need to
@@ -964,7 +965,7 @@ class DatarateTestSVC
int multi_ref, int comp_pred, int is_key_frame,
int dynamic_enable_disable_mode, int rps_mode, int rps_recovery_frame,
int simulcast_mode, bool use_last_as_scaled,
- bool use_last_as_scaled_single_ref) {
+ bool use_last_as_scaled_single_ref, bool disable_last_ref) {
int lag_index = 0;
int base_count = frame_cnt >> 2;
layer_id->spatial_layer_id = spatial_layer;
@@ -1164,6 +1165,11 @@ class DatarateTestSVC
if (dynamic_enable_disable_mode == 1 &&
layer_id->spatial_layer_id == number_spatial_layers_ - 1)
ref_frame_config->reference[0] = 0;
+ // Always disable LAST reference under this flag. use GOLDEN reference.
+ if (disable_last_ref) {
+ ref_frame_config->reference[0] = 0;
+ ref_frame_config->reference[3] = 1;
+ }
return layer_flags;
}
@@ -1508,6 +1514,23 @@ class DatarateTestSVC
CheckDatarate(0.80, 1.60);
}
+ virtual void BasicRateTargetingSVC1TL2SLDisableLASTTest() {
+ SetUpCbr();
+ cfg_.g_error_resilient = 0;
+
+ ::libaom_test::I420VideoSource video("hantro_collage_w352h288.yuv", 352,
+ 288, 30, 1, 0, 300);
+ const int bitrate_array[2] = { 300, 600 };
+ cfg_.rc_target_bitrate = bitrate_array[GET_PARAM(4)];
+ ResetModel();
+ disable_last_ref_ = true;
+ screen_mode_ = true;
+ ASSERT_NO_FATAL_FAILURE(RunLoop(&video));
+#if CONFIG_AV1_DECODER
+ EXPECT_EQ((int)GetMismatchFrames(), 0);
+#endif
+ }
+
virtual void BasicRateTargetingSVC3TL3SLIntraStartDecodeBaseMidSeq() {
SetUpCbr();
cfg_.rc_max_quantizer = 56;
@@ -2380,6 +2403,7 @@ class DatarateTestSVC
int external_resize_pattern_;
bool dynamic_tl_;
bool dynamic_scale_factors_;
+ bool disable_last_ref_;
};
// Check basic rate targeting for CBR, for 3 temporal layers, 1 spatial.
@@ -2458,6 +2482,12 @@ TEST_P(DatarateTestSVC, BasicRateTargetingSVC1TL2SL) {
BasicRateTargetingSVC1TL2SLTest();
}
+// Check basic rate targeting for CBR, for 2 spatial layers, 1 temporal.
+// Disable the usage of LAST referenc frame.
+TEST_P(DatarateTestSVC, BasicRateTargetingSVC1TL2SLDisableLAST) {
+ BasicRateTargetingSVC1TL2SLDisableLASTTest();
+}
+
// Check basic rate targeting for CBR, for 3 spatial layers, 3 temporal,
// with Intra-only frame inserted in the stream. Verify that we can start
// decoding the SL0 stream at the intra_only frame in mid-sequence.

View File

@@ -1,2 +0,0 @@
cherry-pick-ca8a943c247c.patch
cherry-pick-bce2e6728279.patch

View File

@@ -1,36 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Tom Sepez <tsepez@google.com>
Date: Tue, 7 Apr 2026 15:50:30 -0700
Subject: Use safe arithmetic in CFX_PSRenderer::DrawDIBits()
Hardening suggestion from the AI bot.
Bug: 500036290
Change-Id: Ie521629d06ba944f610b941a8c9e9505fa29aea7
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/145731
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Tom Sepez <tsepez@chromium.org>
diff --git a/core/fxge/win32/cfx_psrenderer.cpp b/core/fxge/win32/cfx_psrenderer.cpp
index b38f1a2b7c3271769e609763be2e183f2890ebb3..b8710e50ed01233b2aefbf1760e26e05964b315e 100644
--- a/core/fxge/win32/cfx_psrenderer.cpp
+++ b/core/fxge/win32/cfx_psrenderer.cpp
@@ -620,8 +620,16 @@ bool CFX_PSRenderer::DrawDIBits(RetainPtr<const CFX_DIBBase> bitmap,
encoder_iface_->pJpegEncodeFunc(bitmap, &output_buf, &output_size)) {
filter = "/DCTDecode filter ";
} else {
- int src_pitch = width * bytes_per_pixel;
- output_size = height * src_pitch;
+ FX_SAFE_UINT32 safe_pitch = bytes_per_pixel;
+ safe_pitch *= width;
+ FX_SAFE_UINT32 safe_output_size = safe_pitch;
+ safe_output_size *= height;
+ if (!safe_output_size.IsValid()) {
+ WriteString("\nQ\n");
+ return false;
+ }
+ uint32_t src_pitch = safe_pitch.ValueOrDie();
+ output_size = safe_output_size.ValueOrDie();
output_buf = FX_Alloc(uint8_t, output_size);
for (int row = 0; row < height; row++) {
const uint8_t* src_scan = bitmap->GetScanline(row).data();

View File

@@ -1,70 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Lei Zhang <thestig@chromium.org>
Date: Fri, 27 Mar 2026 14:52:16 -0700
Subject: Patch an overflow in libtiff
Apply fix [1] from upstream, which is not in the most recent versioned
release.
[1] https://gitlab.com/libtiff/libtiff/-/commit/0f726d9
Bug: 496907110
Change-Id: Ic8665879ebdd4445f473e9a1e156cfc42c294d51
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/145550
Reviewed-by: Andy Phan <andyphan@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
diff --git a/third_party/libtiff/0034-tiff-jpeg-overflow.patch b/third_party/libtiff/0034-tiff-jpeg-overflow.patch
new file mode 100644
index 0000000000000000000000000000000000000000..ba6086a38adfa0bd7726affda0f11381e04501e5
--- /dev/null
+++ b/third_party/libtiff/0034-tiff-jpeg-overflow.patch
@@ -0,0 +1,25 @@
+commit 0f726d9477a11e15eb67ca349c03907f6cfb82a9
+Author: Mikhail Khachaiants <mkhachaiants@gmail.com>
+Date: Mon Dec 1 22:26:34 2025 +0200
+
+ tif_jpeg: reject mismatched JPEG data precision to avoid write overflow
+
+ Ensure TIFF BitsPerSample matches both BITS_IN_JSAMPLE and the JPEG
+ header data_precision for JPEG-compressed images. This prevents
+ under-sized scanline buffers that can lead to write buffer overflows
+ in jdcolor.c/null_convert when decoding malformed inputs.
+
+diff --git a/libtiff/tif_jpeg.c b/libtiff/tif_jpeg.c
+index aba5f99b..4d6370b5 100644
+--- a/libtiff/tif_jpeg.c
++++ b/libtiff/tif_jpeg.c
+@@ -1282,7 +1282,8 @@ int TIFFJPEGIsFullStripRequired(TIFF *tif)
+ sp->cinfo.d.data_precision = td->td_bitspersample;
+ sp->cinfo.d.bits_in_jsample = td->td_bitspersample;
+ #else
+- if (sp->cinfo.d.data_precision != td->td_bitspersample)
++ if (td->td_bitspersample != BITS_IN_JSAMPLE ||
++ sp->cinfo.d.data_precision != td->td_bitspersample)
+ {
+ TIFFErrorExtR(tif, module, "Improper JPEG data precision");
+ return (0);
diff --git a/third_party/libtiff/README.pdfium b/third_party/libtiff/README.pdfium
index 9953e767853bcd30683cc24d0d1839c916659185..e3f352d747007641b5d0bd2256a5dbc8af7c20af 100644
--- a/third_party/libtiff/README.pdfium
+++ b/third_party/libtiff/README.pdfium
@@ -19,3 +19,4 @@ Local Modifications:
0028-nstrips-OOM.patch: return error for excess number of tiles/strips.
0031-safe_size_ingtStripContig.patch: return error if the size to read overflow from int32.
0033-avail-out-overflow.patch: signed comparison in PixarLogDecode().
+0034-tiff-jpeg-overflow.patch: reject mismatched JPEG data precision.
diff --git a/third_party/libtiff/tif_jpeg.c b/third_party/libtiff/tif_jpeg.c
index 5281457d936a0dfa5f877c6a7efff6a65066f520..a9764f073db04d6e593e421105d0f59efbfbbeb2 100644
--- a/third_party/libtiff/tif_jpeg.c
+++ b/third_party/libtiff/tif_jpeg.c
@@ -1287,7 +1287,8 @@ int TIFFJPEGIsFullStripRequired(TIFF *tif)
sp->cinfo.d.data_precision = td->td_bitspersample;
sp->cinfo.d.bits_in_jsample = td->td_bitspersample;
#else
- if (sp->cinfo.d.data_precision != td->td_bitspersample)
+ if (td->td_bitspersample != BITS_IN_JSAMPLE ||
+ sp->cinfo.d.data_precision != td->td_bitspersample)
{
TIFFErrorExtR(tif, module, "Improper JPEG data precision");
return (0);

View File

@@ -1,2 +1,2 @@
cherry-pick-0566b2f5f0d1.patch
cherry-pick-3f9969421ad5.patch
cherry-pick-8c705ac86366.patch

View File

@@ -1,651 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Michael Ludwig <michaelludwig@google.com>
Date: Wed, 1 Apr 2026 09:48:48 -0400
Subject: Use 16-bit size for ResourceKeys
Internally, ResourceKey required the size to fit into a uint16_t so this
makes that explicit in the public API. It also changes how the size is
stored to instead record the num32DataCount directly and then convert to
bytes as needed, whereas previously it was requiring that the actual
byte count fit into a uint16_t. This gives a bit more head room.
Call sites to the ResourceKey builders are updated to now have the
responsibility of checking that their size can fit into a uint16_t. For
the most part, these were fixed or trivially small variable key sizes.
The two exceptions were Ganesh's style key (with dashes) and its
inherited key system for shapes with applied styles and path effects.
They now have reasonable limits to prevent the keys from growing bigger
than about 1kb.
Bug: b/495700484
Change-Id: I6ac4f17628b9a2e1a777c473b74e6d1f5c68b27d
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1199497
Reviewed-by: Robert Phillips <robertphillips@google.com>
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
diff --git a/src/gpu/ResourceKey.h b/src/gpu/ResourceKey.h
index f8dee7983036a95d2f5fd7404553916b5c616e83..19851a67653669058361570c615d9be45dc5153a 100644
--- a/src/gpu/ResourceKey.h
+++ b/src/gpu/ResourceKey.h
@@ -19,6 +19,7 @@
#include <cstdint>
#include <cstring>
+#include <limits>
#include <new>
#include <utility>
@@ -77,14 +78,10 @@ public:
}
protected:
- Builder(ResourceKey* key, uint32_t domain, int data32Count) : fKey(key) {
- size_t count = SkToSizeT(data32Count);
+ Builder(ResourceKey* key, uint16_t domain, uint16_t data32Count) : fKey(key) {
SkASSERT(domain != kInvalidDomain);
- key->fKey.reset(kMetaDataCnt + count);
- size_t size = (count + kMetaDataCnt) * sizeof(uint32_t);
- SkASSERT(SkToU16(size) == size);
- SkASSERT(SkToU16(domain) == domain);
- key->fKey[kDomainAndSize_MetaDataIdx] = SkToU32(domain | (size << 16));
+ key->fKey.reset(kMetaDataCnt + data32Count);
+ key->fKey[kDomainAndSize_MetaDataIdx] = domain | (data32Count << 16);
}
private:
@@ -92,7 +89,7 @@ public:
};
protected:
- static const uint32_t kInvalidDomain = 0;
+ static const uint16_t kInvalidDomain = 0;
ResourceKey() { this->reset(); }
@@ -118,10 +115,10 @@ protected:
return *this;
}
- uint32_t domain() const { return fKey[kDomainAndSize_MetaDataIdx] & 0xffff; }
+ uint16_t domain() const { return fKey[kDomainAndSize_MetaDataIdx] & 0xffff; }
/** size of the key data, excluding meta-data (hash, domain, etc). */
- size_t dataSize() const { return this->size() - 4 * kMetaDataCnt; }
+ size_t dataSize() const { return (fKey[kDomainAndSize_MetaDataIdx] >> 16) * sizeof(uint32_t); }
/** ptr to the key data, excluding meta-data (hash, domain, etc). */
const uint32_t* data() const {
@@ -149,14 +146,17 @@ protected:
private:
enum MetaDataIdx {
kHash_MetaDataIdx,
- // The key domain and size are packed into a single uint32_t.
+ // The key domain and size are packed into a single uint32_t. The stored size is in units
+ // of uint32_t and does not include the metadata, i.e. it stores the data32Count provided
+ // to the original key builder.
kDomainAndSize_MetaDataIdx,
kLastMetaDataIdx = kDomainAndSize_MetaDataIdx
};
static const uint32_t kMetaDataCnt = kLastMetaDataIdx + 1;
- size_t internalSize() const { return fKey[kDomainAndSize_MetaDataIdx] >> 16; }
+ // Total size in bytes, including metadata
+ size_t internalSize() const { return this->dataSize() + sizeof(uint32_t) * kMetaDataCnt; }
void validate() const {
SkASSERT(this->isValid());
@@ -197,7 +197,7 @@ private:
class ScratchKey : public ResourceKey {
public:
/** Uniquely identifies the type of resource that is cached as scratch. */
- typedef uint32_t ResourceType;
+ typedef uint16_t ResourceType;
/** Generate a unique ResourceType. */
static ResourceType GenerateResourceType();
@@ -219,7 +219,7 @@ public:
class Builder : public ResourceKey::Builder {
public:
- Builder(ScratchKey* key, ResourceType type, int data32Count)
+ Builder(ScratchKey* key, ResourceType type, uint16_t data32Count)
: ResourceKey::Builder(key, type, data32Count) {}
};
};
@@ -240,7 +240,7 @@ public:
*/
class UniqueKey : public ResourceKey {
public:
- typedef uint32_t Domain;
+ typedef uint16_t Domain;
/** Generate a Domain for unique keys. */
static Domain GenerateDomain();
@@ -279,17 +279,17 @@ public:
class Builder : public ResourceKey::Builder {
public:
- Builder(UniqueKey* key, Domain type, int data32Count, const char* tag = nullptr)
+ Builder(UniqueKey* key, Domain type, uint16_t data32Count, const char* tag = nullptr)
: ResourceKey::Builder(key, type, data32Count) {
key->fTag = tag;
}
/** Used to build a key that wraps another key and adds additional data. */
- Builder(UniqueKey* key, const UniqueKey& innerKey, Domain domain, int extraData32Cnt,
+ Builder(UniqueKey* key, const UniqueKey& innerKey, Domain domain, uint16_t extraData32Cnt,
const char* tag = nullptr)
: ResourceKey::Builder(key,
domain,
- Data32CntForInnerKey(innerKey) + extraData32Cnt) {
+ Data32CntForInnerKey(innerKey, extraData32Cnt)) {
SkASSERT(&innerKey != key);
// add the inner key to the end of the key so that op[] can be indexed normally.
uint32_t* innerKeyData = &this->operator[](extraData32Cnt);
@@ -300,9 +300,15 @@ public:
}
private:
- static int Data32CntForInnerKey(const UniqueKey& innerKey) {
- // key data + domain
- return SkToInt((innerKey.dataSize() >> 2) + 1);
+ static uint16_t Data32CntForInnerKey(const UniqueKey& innerKey, uint16_t extraData32Cnt) {
+ // key data + domain + extraData32Cnt needs to fit into a uint16_t. This key builder is
+ // only used in Ganesh for wrapping textures
+ uint16_t innerData32Cnt = innerKey.dataSize() >> 2;
+ // The Builder API doesn't have a way to return a failure, so if this is somehow
+ // exceeded, then we have no way to recover.
+ SkASSERT_RELEASE((uint32_t) extraData32Cnt + (uint32_t) innerData32Cnt + 1 <=
+ (uint32_t) std::numeric_limits<uint16_t>::max());
+ return innerData32Cnt + extraData32Cnt + 1;
}
};
diff --git a/src/gpu/ganesh/GrStyle.cpp b/src/gpu/ganesh/GrStyle.cpp
index 5d7bc9c1d971bbcf3df0fa720f660f23dfdcbab5..d1bdcf5a117b61f3a8ac3010d6d3cc3702f82ced 100644
--- a/src/gpu/ganesh/GrStyle.cpp
+++ b/src/gpu/ganesh/GrStyle.cpp
@@ -18,8 +18,18 @@
int GrStyle::KeySize(const GrStyle &style, Apply apply, uint32_t flags) {
static_assert(sizeof(uint32_t) == sizeof(SkScalar));
+
+ // We embed the dash interval pattern into the key, and the key size must fit within 16-bits.
+ // However, we put a more conservative upper limit on the dashes because we don't want to keep
+ // key memory locked up in caches during pathological cases.
+ static constexpr int kDashIntervalKeyLimit = 512;
+
int size = 0;
if (style.isDashed()) {
+ if (style.dashIntervalCnt() > kDashIntervalKeyLimit) {
+ return -1; // Disable caching for pathologically large dash patterns
+ }
+
// One scalar for scale, one for dash phase, and one for each dash value.
size += 2 + style.dashIntervalCnt();
} else if (style.pathEffect()) {
diff --git a/src/gpu/ganesh/GrStyle.h b/src/gpu/ganesh/GrStyle.h
index 41b0ce9db13e57db63cd1949dc87b9c20023fcc6..252b975e1e66dd654326449f3c9cbc317b8a2fda 100644
--- a/src/gpu/ganesh/GrStyle.h
+++ b/src/gpu/ganesh/GrStyle.h
@@ -74,6 +74,8 @@ public:
* into a key. This occurs when there is a path effect that is not a dash. The key can
* either reflect just the path effect (if one) or the path effect and the strokerec. Note
* that a simple fill has a zero sized key.
+ *
+ * If a positive value is returned, it will fit in a uint16_t.
*/
static int KeySize(const GrStyle&, Apply, uint32_t flags = 0);
diff --git a/src/gpu/ganesh/geometry/GrStyledShape.cpp b/src/gpu/ganesh/geometry/GrStyledShape.cpp
index 3c2b942aa6c614a6312e6695309cb9fc8dd6f5d5..6aa4daa3f8d76ab0dfe062dfb2f4b1503a8a5e1f 100644
--- a/src/gpu/ganesh/geometry/GrStyledShape.cpp
+++ b/src/gpu/ganesh/geometry/GrStyledShape.cpp
@@ -19,6 +19,7 @@
#include <algorithm>
#include <cstring>
+#include <limits>
#include <utility>
@@ -141,12 +142,12 @@ static void write_path_key_from_data(const SkPath& path, uint32_t* origKey) {
SkASSERT(key - origKey == path_key_from_data_size(path));
}
-int GrStyledShape::unstyledKeySize() const {
+uint16_t GrStyledShape::unstyledKeySize() const {
if (fInheritedKey.count()) {
- return fInheritedKey.count();
+ return SkTo<uint16_t>(fInheritedKey.count());
}
- int count = 1; // Every key has the state flags from the GrShape
+ uint16_t count = 1; // Every key has the state flags from the GrShape
switch(fShape.type()) {
case GrShape::Type::kPoint:
static_assert(0 == sizeof(SkPoint) % sizeof(uint32_t));
@@ -170,11 +171,13 @@ int GrStyledShape::unstyledKeySize() const {
break;
case GrShape::Type::kPath: {
if (0 == fGenID) {
- return -1; // volatile, so won't be keyed
+ return 0; // volatile, so won't be keyed
}
+ // When >= 0, `dataKeySize` is a reasonably small number bounded by
+ // kMaxKeyFromDataVerbCnt since point count is derived from verb count.
int dataKeySize = path_key_from_data_size(fShape.path());
if (dataKeySize >= 0) {
- count += dataKeySize;
+ count += SkTo<uint16_t>(dataKeySize);
} else {
count++; // Just adds the gen ID.
}
@@ -251,6 +254,7 @@ void GrStyledShape::writeUnstyledKey(uint32_t* key) const {
void GrStyledShape::setInheritedKey(const GrStyledShape &parent, GrStyle::Apply apply,
SkScalar scale) {
+ static constexpr int kInheritedKeyLimit = 1024;
SkASSERT(!fInheritedKey.count());
// If the output shape turns out to be simple, then we will just use its geometric key
if (fShape.isPath()) {
@@ -264,7 +268,7 @@ void GrStyledShape::setInheritedKey(const GrStyledShape &parent, GrStyle::Apply
bool useParentGeoKey = !parentCnt;
if (useParentGeoKey) {
parentCnt = parent.unstyledKeySize();
- if (parentCnt < 0) {
+ if (!parentCnt) {
// The parent's geometry has no key so we will have no key.
fGenID = 0;
return;
@@ -283,7 +287,12 @@ void GrStyledShape::setInheritedKey(const GrStyledShape &parent, GrStyle::Apply
// we try to get a key for the shape.
fGenID = 0;
return;
+ } else if (parentCnt + styleCnt > kInheritedKeyLimit) {
+ // Prevent chained path effects and styles from growing the key too large
+ fGenID = 0;
+ return;
}
+
fInheritedKey.reset(parentCnt + styleCnt);
if (useParentGeoKey) {
// This will be the geo key.
diff --git a/src/gpu/ganesh/geometry/GrStyledShape.h b/src/gpu/ganesh/geometry/GrStyledShape.h
index 97db583a5cf8aa8c7fe8c0e054ef622ca6945744..ed4345355478121dfedb1a894e159b80c4fca304 100644
--- a/src/gpu/ganesh/geometry/GrStyledShape.h
+++ b/src/gpu/ganesh/geometry/GrStyledShape.h
@@ -252,11 +252,11 @@ public:
/**
* Gets the size of the key for the shape represented by this GrStyledShape (ignoring its
- * styling). A negative value is returned if the shape has no key (shouldn't be cached).
+ * styling). A zero value is returned if the shape has no key (shouldn't be cached).
*/
- int unstyledKeySize() const;
+ uint16_t unstyledKeySize() const;
- bool hasUnstyledKey() const { return this->unstyledKeySize() >= 0; }
+ bool hasUnstyledKey() const { return this->unstyledKeySize() > 0; }
/**
* Writes unstyledKeySize() bytes into the provided pointer. Assumes that there is enough
diff --git a/src/gpu/ganesh/image/GrImageUtils.cpp b/src/gpu/ganesh/image/GrImageUtils.cpp
index a88ff15c0a59f1a5f417aa37f243385503a3dfd9..8fd9ea6a55e4a77c793c7af2361aab7e06c63f08 100644
--- a/src/gpu/ganesh/image/GrImageUtils.cpp
+++ b/src/gpu/ganesh/image/GrImageUtils.cpp
@@ -683,8 +683,7 @@ GrSurfaceProxyView FindOrMakeCachedMipmappedView(GrRecordingContext* rContext,
SkASSERT(baseKey.isValid());
skgpu::UniqueKey mipmappedKey;
static const skgpu::UniqueKey::Domain kMipmappedDomain = skgpu::UniqueKey::GenerateDomain();
- { // No extra values beyond the domain are required. Must name the var to please
- // clang-tidy.
+ { // No extra values beyond the domain are required. Must name the var to please clang-tidy.
skgpu::UniqueKey::Builder b(&mipmappedKey, baseKey, kMipmappedDomain, 0);
}
SkASSERT(mipmappedKey.isValid());
diff --git a/src/gpu/ganesh/ops/TriangulatingPathRenderer.cpp b/src/gpu/ganesh/ops/TriangulatingPathRenderer.cpp
index 124e51842eafbf7d3b010ca3232731d549515a9b..1bf3038847223c8167fa60c852c23216d186c5cc 100644
--- a/src/gpu/ganesh/ops/TriangulatingPathRenderer.cpp
+++ b/src/gpu/ganesh/ops/TriangulatingPathRenderer.cpp
@@ -282,8 +282,7 @@ private:
bool inverseFill = shape.inverseFilled();
static constexpr int kClipBoundsCnt = sizeof(devClipBounds) / sizeof(uint32_t);
- int shapeKeyDataCnt = shape.unstyledKeySize();
- SkASSERT(shapeKeyDataCnt >= 0);
+ uint16_t shapeKeyDataCnt = shape.unstyledKeySize();
skgpu::UniqueKey::Builder builder(key, kDomain, shapeKeyDataCnt + kClipBoundsCnt, "Path");
shape.writeUnstyledKey(&builder[0]);
// For inverse fills, the tessellation is dependent on clip bounds.
diff --git a/src/gpu/graphite/GraphiteResourceKey.h b/src/gpu/graphite/GraphiteResourceKey.h
index 12e72d1b24f45ac5885a125cb3d52cb648a55402..d52f0099a722aaf2a4b655abf6bf8c0ea26dcf57 100644
--- a/src/gpu/graphite/GraphiteResourceKey.h
+++ b/src/gpu/graphite/GraphiteResourceKey.h
@@ -46,7 +46,7 @@ public:
class Builder : public ResourceKey::Builder {
public:
- Builder(GraphiteResourceKey* key, ResourceType type, int data32Count)
+ Builder(GraphiteResourceKey* key, ResourceType type, uint16_t data32Count)
: ResourceKey::Builder(key, type, data32Count) {}
};
};
diff --git a/src/gpu/graphite/RasterPathUtils.cpp b/src/gpu/graphite/RasterPathUtils.cpp
index 1d8b5563e41bf8e3f515438c666760b228c3947c..8557d35bc4803cf71224457d103c3a9ce874012c 100644
--- a/src/gpu/graphite/RasterPathUtils.cpp
+++ b/src/gpu/graphite/RasterPathUtils.cpp
@@ -140,7 +140,7 @@ skgpu::UniqueKey GeneratePathMaskKey(const Shape& shape,
skgpu::UniqueKey maskKey;
{
static const skgpu::UniqueKey::Domain kDomain = skgpu::UniqueKey::GenerateDomain();
- int styleKeySize = 7;
+ uint16_t styleKeySize = 7;
if (!strokeRec.isHairlineStyle() && !strokeRec.isFillStyle()) {
// Add space for width and miter if needed
styleKeySize += 2;
@@ -185,66 +185,56 @@ skgpu::UniqueKey GenerateClipMaskKey(uint32_t stackRecordID,
skgpu::UniqueKey maskKey;
// if the element list is too large we just use the stackRecordID
if (elementsForMask->size() <= kMaxShapeCountForKey) {
- constexpr int kXformKeySize = 5;
- int keySize = 0;
- bool canCreateKey = true;
- // Iterate through to get key size and see if we can create a key at all
+ static constexpr int kXformKeySize = 5;
+ uint16_t keySize = includeBounds ? 2 : 0;
+ // Iterate through to get key size; given kMaxShapeCountForKey and Shape's own key size
+ // limitations, this should always fit safely within a 16-bit number
for (int i = 0; i < elementsForMask->size(); ++i) {
- int shapeKeySize = (*elementsForMask)[i]->fShape.keySize();
- if (shapeKeySize < 0) {
- canCreateKey = false;
- break;
- }
- keySize += kXformKeySize + shapeKeySize;
+ keySize += kXformKeySize + (*elementsForMask)[i]->fShape.keySize();
}
- if (canCreateKey) {
- if (includeBounds) {
- keySize += 2;
- }
- skgpu::UniqueKey::Builder builder(&maskKey, kDomain, keySize,
- "Clip Path Mask");
- int elementKeyIndex = 0;
- Rect unclippedBounds = Rect::InfiniteInverted();
- for (int i = 0; i < elementsForMask->size(); ++i) {
- const ClipStack::Element* element = (*elementsForMask)[i];
-
- // Add transform key and get packed fractional translation bits
- uint32_t fracBits = add_transform_key(&builder,
- elementKeyIndex,
- element->fLocalToDevice);
- uint32_t opBits = static_cast<uint32_t>(element->fOp);
- builder[elementKeyIndex + 4] = fracBits | (opBits << 16);
-
- const Shape& shape = element->fShape;
- shape.writeKey(&builder[elementKeyIndex + kXformKeySize],
- /*includeInverted=*/true);
-
- elementKeyIndex += kXformKeySize + shape.keySize();
-
- Rect transformedBounds = element->fLocalToDevice.mapRect(element->fShape.bounds());
- unclippedBounds.join(transformedBounds);
- }
-
- // The keyBounds are the maskDeviceBounds relative to the full transformed mask. We use
- // this to ensure we capture the situation where the maskDeviceBounds are equal in two
- // cases but actually enclose different regions of the full mask due to an integer
- // translation (which is not captured in the key) in the element transforms.
- *keyBounds = maskDeviceBounds.makeOffset(-unclippedBounds.left(),
- -unclippedBounds.top());
-
- if (includeBounds) {
- SkASSERT(SkTFitsIn<int16_t>(keyBounds->left()));
- SkASSERT(SkTFitsIn<int16_t>(keyBounds->top()));
- SkASSERT(SkTFitsIn<int16_t>(keyBounds->right()));
- SkASSERT(SkTFitsIn<int16_t>(keyBounds->bottom()));
-
- builder[elementKeyIndex] = keyBounds->left() | (keyBounds->top() << 16);
- builder[elementKeyIndex+1] = keyBounds->right() | (keyBounds->bottom() << 16);
- }
-
- *usesPathKey = true;
- return maskKey;
+
+ skgpu::UniqueKey::Builder builder(&maskKey, kDomain, keySize, "Clip Path Mask");
+ int elementKeyIndex = 0;
+ Rect unclippedBounds = Rect::InfiniteInverted();
+ for (int i = 0; i < elementsForMask->size(); ++i) {
+ const ClipStack::Element* element = (*elementsForMask)[i];
+
+ // Add transform key and get packed fractional translation bits
+ uint32_t fracBits = add_transform_key(&builder,
+ elementKeyIndex,
+ element->fLocalToDevice);
+ uint32_t opBits = static_cast<uint32_t>(element->fOp);
+ builder[elementKeyIndex + 4] = fracBits | (opBits << 16);
+
+ const Shape& shape = element->fShape;
+ shape.writeKey(&builder[elementKeyIndex + kXformKeySize],
+ /*includeInverted=*/true);
+
+ elementKeyIndex += kXformKeySize + shape.keySize();
+
+ Rect transformedBounds = element->fLocalToDevice.mapRect(element->fShape.bounds());
+ unclippedBounds.join(transformedBounds);
+ }
+
+ // The keyBounds are the maskDeviceBounds relative to the full transformed mask. We use
+ // this to ensure we capture the situation where the maskDeviceBounds are equal in two
+ // cases but actually enclose different regions of the full mask due to an integer
+ // translation (which is not captured in the key) in the element transforms.
+ *keyBounds = maskDeviceBounds.makeOffset(-unclippedBounds.left(),
+ -unclippedBounds.top());
+
+ if (includeBounds) {
+ SkASSERT(SkTFitsIn<int16_t>(keyBounds->left()));
+ SkASSERT(SkTFitsIn<int16_t>(keyBounds->top()));
+ SkASSERT(SkTFitsIn<int16_t>(keyBounds->right()));
+ SkASSERT(SkTFitsIn<int16_t>(keyBounds->bottom()));
+
+ builder[elementKeyIndex] = keyBounds->left() | (keyBounds->top() << 16);
+ builder[elementKeyIndex+1] = keyBounds->right() | (keyBounds->bottom() << 16);
}
+
+ *usesPathKey = true;
+ return maskKey;
}
// Either we have too many elements or at least one shape can't create a key
diff --git a/src/gpu/graphite/ResourceProvider.cpp b/src/gpu/graphite/ResourceProvider.cpp
index cd67a8af6fe60c2239dc6cfc7adaa8604ef3743a..80ce839d942b96f6ba7eb0456881a986accfe145 100644
--- a/src/gpu/graphite/ResourceProvider.cpp
+++ b/src/gpu/graphite/ResourceProvider.cpp
@@ -177,7 +177,7 @@ sk_sp<Sampler> ResourceProvider::findOrCreateCompatibleSampler(const SamplerDesc
// immutable sampler details into the SamplerDesc, so there is no need to delegate to Caps
// to create a specific key.
const SkSpan<const uint32_t>& samplerData = samplerDesc.asSpan();
- GraphiteResourceKey::Builder builder(&key, kType, samplerData.size());
+ GraphiteResourceKey::Builder builder(&key, kType, SkTo<uint16_t>(samplerData.size()));
for (size_t i = 0; i < samplerData.size(); i++) {
builder[i] = samplerData[i];
@@ -231,8 +231,8 @@ sk_sp<Buffer> ResourceProvider::findOrCreateBuffer(
// For the key we need ((sizeof(size_t) + (sizeof(uint32_t) - 1)) / (sizeof(uint32_t))
// uint32_t's for the size and one uint32_t for the rest.
static_assert(sizeof(uint32_t) == 4);
- static const int kSizeKeyNum32DataCnt = (sizeof(size_t) + 3) / 4;
- static const int kKeyNum32DataCnt = kSizeKeyNum32DataCnt + 1;
+ static const uint16_t kSizeKeyNum32DataCnt = (sizeof(size_t) + 3) / 4;
+ static const uint16_t kKeyNum32DataCnt = kSizeKeyNum32DataCnt + 1;
SkASSERT(static_cast<uint32_t>(type) < (1u << 4));
SkASSERT(static_cast<uint32_t>(accessPattern) < (1u << 2));
diff --git a/src/gpu/graphite/dawn/DawnCaps.cpp b/src/gpu/graphite/dawn/DawnCaps.cpp
index 3717790e1413401c0fbf12ff4cebd31132153ffd..1c281e3e7fd0299fb14d7fa8c763690bf68bae6b 100644
--- a/src/gpu/graphite/dawn/DawnCaps.cpp
+++ b/src/gpu/graphite/dawn/DawnCaps.cpp
@@ -1016,7 +1016,7 @@ uint32_t DawnCaps::getRenderPassDescKeyForPipeline(const RenderPassDesc& renderP
loadResolveAttachmentKey;
}
-static constexpr int kDawnGraphicsPipelineKeyData32Count = 4;
+static constexpr uint16_t kDawnGraphicsPipelineKeyData32Count = 4;
UniqueKey DawnCaps::makeGraphicsPipelineKey(const GraphicsPipelineDesc& pipelineDesc,
const RenderPassDesc& renderPassDesc) const {
@@ -1234,7 +1234,7 @@ void DawnCaps::buildKeyForTexture(SkISize dimensions,
SkASSERT(static_cast<uint32_t>(dawnInfo.fUsage) < (1u << 28)); // usage is remaining 28 bits
// We need two uint32_ts for dimensions, 1 for format, and 1 for the rest of the key;
- int num32DataCnt = 2 + 1 + 1;
+ uint16_t num32DataCnt = 2 + 1 + 1;
bool hasYcbcrInfo = false;
#if !defined(__EMSCRIPTEN__)
// If we are using ycbcr texture/sampling, more key information is needed.
diff --git a/src/gpu/graphite/geom/AnalyticBlurMask.cpp b/src/gpu/graphite/geom/AnalyticBlurMask.cpp
index 97f38ba054f66249d70c739cea0318f4d3e30203..5a118bf8b1792be4400a5a43db2d936366157fd0 100644
--- a/src/gpu/graphite/geom/AnalyticBlurMask.cpp
+++ b/src/gpu/graphite/geom/AnalyticBlurMask.cpp
@@ -375,7 +375,7 @@ std::optional<AnalyticBlurMask> AnalyticBlurMask::MakeRRect(Recorder* recorder,
static const UniqueKey::Domain kRRectBlurDomain = UniqueKey::GenerateDomain();
UniqueKey key;
{
- static constexpr int kKeySize = sizeof(DerivedParams) / sizeof(uint32_t);
+ static constexpr uint16_t kKeySize = sizeof(DerivedParams) / sizeof(uint32_t);
static_assert(SkIsAlign4(sizeof(DerivedParams)));
// TODO: We should discretize the sigma to perceptibly meaningful changes to the table,
// as well as the underlying the round rect geometry.
diff --git a/src/gpu/graphite/geom/Shape.cpp b/src/gpu/graphite/geom/Shape.cpp
index 29898fb00507aa64317ffd78354f36a18ca0a7b0..2465dcfb9fc92b678e67520e7b0e104d8514c147 100644
--- a/src/gpu/graphite/geom/Shape.cpp
+++ b/src/gpu/graphite/geom/Shape.cpp
@@ -183,8 +183,8 @@ void write_path_key_from_data(const SkPath& path, uint32_t* origKey) {
}
} // anonymous namespace
-int Shape::keySize() const {
- int count = 1; // Every key has the state flags from the Shape
+uint16_t Shape::keySize() const {
+ uint16_t count = 1; // Every key has the state flags from the Shape
switch(this->type()) {
case Type::kLine:
static_assert(0 == sizeof(skvx::float4) % sizeof(uint32_t));
@@ -207,7 +207,7 @@ int Shape::keySize() const {
if (!this->path().isEmpty()) {
int dataKeySize = path_key_from_data_size(this->path());
if (dataKeySize >= 0) {
- count += dataKeySize;
+ count += SkTo<uint16_t>(dataKeySize);
} else {
count++; // Just adds the gen ID.
}
diff --git a/src/gpu/graphite/geom/Shape.h b/src/gpu/graphite/geom/Shape.h
index 8c02945b7090779385a82aa4a8d257dad758e331..30dd8fffb797aee0c8a85093f5d5cdb97ef1a0fa 100644
--- a/src/gpu/graphite/geom/Shape.h
+++ b/src/gpu/graphite/geom/Shape.h
@@ -184,7 +184,7 @@ public:
/**
* Gets the size of the key for the shape represented by this Shape.
*/
- int keySize() const;
+ uint16_t keySize() const;
/**
* Writes keySize() bytes into the provided pointer. Assumes that there is enough
diff --git a/src/gpu/graphite/mtl/MtlCaps.mm b/src/gpu/graphite/mtl/MtlCaps.mm
index 7816ca699def160c1aa56afb28463d3c9d77926f..e5939af8fef877c297e98195e111860c88b0e876 100644
--- a/src/gpu/graphite/mtl/MtlCaps.mm
+++ b/src/gpu/graphite/mtl/MtlCaps.mm
@@ -949,7 +949,7 @@ MTLPixelFormat format_from_compression(SkTextureCompressionType compression) {
return {formatInfo.fColorTypeInfos.get(), formatInfo.fColorTypeInfoCount};
}
-static constexpr int kMtlGraphicsPipelineKeyData32Count = 4;
+static constexpr uint16_t kMtlGraphicsPipelineKeyData32Count = 4;
UniqueKey MtlCaps::makeGraphicsPipelineKey(const GraphicsPipelineDesc& pipelineDesc,
const RenderPassDesc& renderPassDesc) const {
@@ -1193,7 +1193,7 @@ MTLPixelFormat format_from_compression(SkTextureCompressionType compression) {
SkASSERT(static_cast<uint32_t>(isFBOnly) < (1u << 1));
// We need two uint32_ts for dimensions, 2 for format, and 1 for the rest of the key;
- static int kNum32DataCnt = 2 + 2 + 1;
+ static uint16_t kNum32DataCnt = 2 + 2 + 1;
GraphiteResourceKey::Builder builder(key, type, kNum32DataCnt);
diff --git a/src/gpu/graphite/vk/VulkanCaps.cpp b/src/gpu/graphite/vk/VulkanCaps.cpp
index 799d90b03c54cee89b24281e25c46813adef5046..f5ae0b882af66050b3e90c121675ab1b3a4ec870 100644
--- a/src/gpu/graphite/vk/VulkanCaps.cpp
+++ b/src/gpu/graphite/vk/VulkanCaps.cpp
@@ -2087,7 +2087,7 @@ bool VulkanCaps::msaaTextureRenderToSingleSampledSupport(const TextureInfo& info
// 4 uint32s for the render step id, paint id, compatible render pass description, and write
// swizzle.
-static constexpr int kPipelineKeyData32Count = 4;
+static constexpr uint16_t kPipelineKeyData32Count = 4;
static constexpr int kPipelineKeyRenderStepIDIndex = 0;
static constexpr int kPipelineKeyPaintParamsIDIndex = 1;
@@ -2173,15 +2173,15 @@ void VulkanCaps::buildKeyForTexture(SkISize dimensions,
SkASSERT(vkInfo.fAspectMask < (1u << 11)); // aspectMask is bits 8 - 19
// We need two uint32_ts for dimensions and 3 for miscellaneous information.
- static constexpr int kNum32DimensionDataCnt = 2;
- static constexpr int kNum32MiscDataCnt = 3;
+ static constexpr uint16_t kNum32DimensionDataCnt = 2;
+ static constexpr uint16_t kNum32MiscDataCnt = 3;
// Non-YCbCr formats need 1 int for format.
// YCbCr conversion needs 1 int for non-format flags, and a 64-bit format (external or regular).
- static constexpr int kNum32FormatDataCntNoYcbcr = 1;
- static constexpr int kNum32FormatDataCntYcbcr = 3;
+ static constexpr uint16_t kNum32FormatDataCntNoYcbcr = 1;
+ static constexpr uint16_t kNum32FormatDataCntYcbcr = 3;
const VulkanYcbcrConversionInfo& ycbcrInfo = vkInfo.fYcbcrConversionInfo;
- const int num32DataCnt =
+ const uint16_t num32DataCnt =
kNum32DimensionDataCnt + kNum32MiscDataCnt +
(ycbcrInfo.isValid() ? kNum32FormatDataCntYcbcr : kNum32FormatDataCntNoYcbcr);
diff --git a/src/gpu/graphite/vk/VulkanResourceProvider.cpp b/src/gpu/graphite/vk/VulkanResourceProvider.cpp
index bb2f400250c569b73120f857133cafcca51c1c77..f66c6d0d2cf8c8bb4b34c7479445185c3a3c7cf4 100644
--- a/src/gpu/graphite/vk/VulkanResourceProvider.cpp
+++ b/src/gpu/graphite/vk/VulkanResourceProvider.cpp
@@ -218,7 +218,7 @@ GraphiteResourceKey build_desc_set_key(const SkSpan<DescriptorData>& requestedDe
}
GraphiteResourceKey key;
- GraphiteResourceKey::Builder builder(&key, kType, keyData.size());
+ GraphiteResourceKey::Builder builder(&key, kType, SkTo<uint16_t>(keyData.size()));
for (int i = 0; i < keyData.size(); i++) {
builder[i] = keyData[i];
@@ -548,7 +548,7 @@ sk_sp<VulkanYcbcrConversion> VulkanResourceProvider::findOrCreateCompatibleYcbcr
GraphiteResourceKey key;
{
static const ResourceType kType = GraphiteResourceKey::GenerateResourceType();
- static constexpr int kKeySize = 3;
+ static constexpr uint16_t kKeySize = 3;
GraphiteResourceKey::Builder builder(&key, kType, kKeySize);
ImmutableSamplerInfo packedInfo = VulkanYcbcrConversion::ToImmutableSamplerInfo(ycbcrInfo);
diff --git a/src/utils/SkShadowUtils.cpp b/src/utils/SkShadowUtils.cpp
index 68da13ca9b048cd0d1dc9b62090c17793a11b3a1..9c3ef544ffda6ba72c2972f020ac2a06232484b6 100644
--- a/src/utils/SkShadowUtils.cpp
+++ b/src/utils/SkShadowUtils.cpp
@@ -358,7 +358,10 @@ public:
const SkMatrix& viewMatrix() const { return *fViewMatrix; }
#if defined(SK_GANESH)
/** Negative means the vertices should not be cached for this path. */
- int keyBytes() const { return fShapeForKey.unstyledKeySize() * sizeof(uint32_t); }
+ int keyBytes() const {
+ return fShapeForKey.hasUnstyledKey() ? fShapeForKey.unstyledKeySize() * sizeof(uint32_t)
+ : -1;
+ }
void writeKey(void* key) const {
fShapeForKey.writeUnstyledKey(reinterpret_cast<uint32_t*>(key));
}

View File

@@ -0,0 +1,101 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Stephen Nusko <nuskos@google.com>
Date: Tue, 7 Apr 2026 14:00:00 -0700
Subject: Use SkSafeMath to prevent overflow in pixel offset calculations.
The trim methods in SkReadPixelsRec and SkWritePixelsRec now use
SkSafeMath to calculate the offset for fPixels. This addresses potential
integer overflows when computing the y and x offsets and their sum.
Additionally, a check for fInfo.minRowBytes() == 0 is added, as
minRowBytes() returns 0 on overflow. If any overflow occurs during
offset calculation, trim will now return false.
See linked bug for potential security issue that motivated this.
And see (sorry internal only) go/code-terracotta-review-explainer for
evaluting this phase (1) patch.
Bug:b/495534710
Change-Id: I0b2a684b5ad1105c7d25418556e40b4d9f511daf
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1194336
Auto-Submit: Stephen Nusko <nuskos@google.com>
Commit-Queue: Stephen Nusko <nuskos@google.com>
Reviewed-by: Kaylee Lubick <kjlubick@google.com>
Reviewed-by: Florin Malita <fmalita@google.com>
diff --git a/src/core/SkReadPixelsRec.cpp b/src/core/SkReadPixelsRec.cpp
index 505bfb51b34a6946b878f4c2d0c1ee0bb86b2cdb..f7e9661629e28a80c6d6d32100c80913806f6917 100644
--- a/src/core/SkReadPixelsRec.cpp
+++ b/src/core/SkReadPixelsRec.cpp
@@ -7,9 +7,12 @@
#include "src/core/SkReadPixelsRec.h"
#include "include/core/SkRect.h"
+#include "src/base/SkSafeMath.h"
bool SkReadPixelsRec::trim(int srcWidth, int srcHeight) {
- if (nullptr == fPixels || fRowBytes < fInfo.minRowBytes()) {
+ // fInfo.minRowBytes() returns 0 if the size doesn't fit in `size_t`.
+ const size_t minRowBytes = fInfo.minRowBytes();
+ if (nullptr == fPixels || fRowBytes < minRowBytes || minRowBytes == 0) {
return false;
}
if (0 >= fInfo.width() || 0 >= fInfo.height()) {
@@ -30,9 +33,17 @@ bool SkReadPixelsRec::trim(int srcWidth, int srcHeight) {
if (y > 0) {
y = 0;
}
- // here x,y are either 0 or negative
+ // here x,y are either 0 or negative (safe to cast to size_t)
// we negate and add them so UBSAN (pointer-overflow) doesn't get confused.
- fPixels = ((char*)fPixels + -y*fRowBytes + -x*fInfo.bytesPerPixel());
+ SkSafeMath safeMath;
+ const size_t y_offset = safeMath.mul(-y, fRowBytes);
+ const size_t x_offset = safeMath.mul(-x, fInfo.bytesPerPixel());
+ const size_t total = safeMath.add(y_offset, x_offset);
+ if (!safeMath.ok()) {
+ return false;
+ }
+
+ fPixels = ((char*)fPixels + total);
// the intersect may have shrunk info's logical size
fInfo = fInfo.makeDimensions(srcR.size());
fX = srcR.x();
diff --git a/src/core/SkWritePixelsRec.cpp b/src/core/SkWritePixelsRec.cpp
index 20b2003f59d265a84b6a24413374789663b5481e..d1bad0f51e6ad208edb39f0c0c473e0fa177d119 100644
--- a/src/core/SkWritePixelsRec.cpp
+++ b/src/core/SkWritePixelsRec.cpp
@@ -8,9 +8,12 @@
#include "src/core/SkWritePixelsRec.h"
#include "include/core/SkRect.h"
+#include "src/base/SkSafeMath.h"
bool SkWritePixelsRec::trim(int dstWidth, int dstHeight) {
- if (nullptr == fPixels || fRowBytes < fInfo.minRowBytes()) {
+ // fInfo.minRowBytes() returns 0 if the size doesn't fit in `size_t`.
+ const size_t minRowBytes = fInfo.minRowBytes();
+ if (nullptr == fPixels || fRowBytes < minRowBytes || minRowBytes == 0) {
return false;
}
if (0 >= fInfo.width() || 0 >= fInfo.height()) {
@@ -31,9 +34,17 @@ bool SkWritePixelsRec::trim(int dstWidth, int dstHeight) {
if (y > 0) {
y = 0;
}
- // here x,y are either 0 or negative
+ // here x,y are either 0 or negative (safe to cast to size_t)
// we negate and add them so UBSAN (pointer-overflow) doesn't get confused.
- fPixels = ((const char*)fPixels + -y*fRowBytes + -x*fInfo.bytesPerPixel());
+ SkSafeMath safeMath;
+ const size_t y_offset = safeMath.mul(-y, fRowBytes);
+ const size_t x_offset = safeMath.mul(-x, fInfo.bytesPerPixel());
+ const size_t total = safeMath.add(y_offset, x_offset);
+ if (!safeMath.ok()) {
+ return false;
+ }
+
+ fPixels = ((const char*)fPixels + total);
// the intersect may have shrunk info's logical size
fInfo = fInfo.makeDimensions(dstR.size());
fX = dstR.x();

View File

@@ -18,10 +18,10 @@ Commit-Queue: Victor Gomes <victorgomes@chromium.org>
Cr-Commit-Position: refs/heads/main@{#106064}
diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index 6b754b4d8592f0214f731dc9b298b3aab59527f2..7d47edc6dfbe5daa5e89647ea38c4e8ec21a522c 100644
index d0796cd1a1bfd74b0ff506f8b1ff840bb6fc964b..6bcf17950efd04b62e4019a639c6ad90fa41875c 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -4646,19 +4646,20 @@ void MaglevGraphBuilder::BuildInitializeStore_TrustedPointer(
@@ -4653,19 +4653,20 @@ void MaglevGraphBuilder::BuildInitializeStore_TrustedPointer(
CHECK(!result.IsDoneWithAbort());
}
@@ -48,7 +48,7 @@ index 6b754b4d8592f0214f731dc9b298b3aab59527f2..7d47edc6dfbe5daa5e89647ea38c4e8e
bool VerifyIsNotEscaping(VirtualObjectList vos, InlinedAllocation* alloc) {
for (VirtualObject* vo : vos) {
if (vo->allocation() == alloc) continue;
@@ -4668,7 +4669,7 @@ bool VerifyIsNotEscaping(VirtualObjectList vos, InlinedAllocation* alloc) {
@@ -4675,7 +4676,7 @@ bool VerifyIsNotEscaping(VirtualObjectList vos, InlinedAllocation* alloc) {
if (!nested_value->Is<InlinedAllocation>()) return true;
ValueNode* nested_alloc = nested_value->Cast<InlinedAllocation>();
if (nested_alloc == alloc) {
@@ -57,7 +57,7 @@ index 6b754b4d8592f0214f731dc9b298b3aab59527f2..7d47edc6dfbe5daa5e89647ea38c4e8e
!VerifyIsNotEscaping(vos, vo->allocation())) {
escaped = true;
}
@@ -4687,6 +4688,7 @@ bool MaglevGraphBuilder::CanTrackObjectChanges(ValueNode* receiver,
@@ -4694,6 +4695,7 @@ bool MaglevGraphBuilder::CanTrackObjectChanges(ValueNode* receiver,
if (!v8_flags.maglev_object_tracking) return false;
if (!receiver->Is<InlinedAllocation>()) return false;
InlinedAllocation* alloc = receiver->Cast<InlinedAllocation>();
@@ -65,7 +65,7 @@ index 6b754b4d8592f0214f731dc9b298b3aab59527f2..7d47edc6dfbe5daa5e89647ea38c4e8e
if (mode == TrackObjectMode::kStore) {
// If we have two objects A and B, such that A points to B (it contains B in
// one of its field), we cannot change B without also changing A, even if
@@ -4695,7 +4697,6 @@ bool MaglevGraphBuilder::CanTrackObjectChanges(ValueNode* receiver,
@@ -4702,7 +4704,6 @@ bool MaglevGraphBuilder::CanTrackObjectChanges(ValueNode* receiver,
graph_->allocations_elide_map().end()) {
return false;
}
@@ -73,7 +73,7 @@ index 6b754b4d8592f0214f731dc9b298b3aab59527f2..7d47edc6dfbe5daa5e89647ea38c4e8e
// Ensure object is escaped if we are within a try-catch block. This is
// crucial because a deoptimization point inside the catch handler could
// re-materialize objects differently, depending on whether the throw
@@ -4704,9 +4705,6 @@ bool MaglevGraphBuilder::CanTrackObjectChanges(ValueNode* receiver,
@@ -4711,9 +4712,6 @@ bool MaglevGraphBuilder::CanTrackObjectChanges(ValueNode* receiver,
// the try-block started, but for now, err on the side of caution and
// always escape.
if (IsInsideTryBlock()) return false;
@@ -83,7 +83,7 @@ index 6b754b4d8592f0214f731dc9b298b3aab59527f2..7d47edc6dfbe5daa5e89647ea38c4e8e
}
// We don't support loop phis inside VirtualObjects, so any access inside a
// loop should escape the object, except for objects that were created since
@@ -9195,7 +9193,7 @@ MaybeReduceResult MaglevGraphBuilder::TryReduceArrayIteratorPrototypeNext(
@@ -9202,7 +9200,7 @@ MaybeReduceResult MaglevGraphBuilder::TryReduceArrayIteratorPrototypeNext(
VirtualObject* array = iterated_object->Cast<InlinedAllocation>()->object();
// TODO(victorgomes): Remove this once we track changes in the inlined
// allocated object.
@@ -92,7 +92,7 @@ index 6b754b4d8592f0214f731dc9b298b3aab59527f2..7d47edc6dfbe5daa5e89647ea38c4e8e
FAIL("allocation is escaping, map could have been changed");
}
// TODO(victorgomes): This effectively disable the optimization for `for-of`
@@ -12315,7 +12313,7 @@ MaglevGraphBuilder::TryGetNonEscapingArgumentsObject(ValueNode* value) {
@@ -12322,7 +12320,7 @@ MaglevGraphBuilder::TryGetNonEscapingArgumentsObject(ValueNode* value) {
}
// TODO(victorgomes): We can probably loosen the IsNotEscaping requirement if
// we keep track of the arguments object changes so far.

View File

@@ -12,7 +12,7 @@ Commit-Queue: Darius Mercadier <dmercadier@chromium.org>
Cr-Commit-Position: refs/heads/main@{#105381}
diff --git a/src/maglev/maglev-graph-builder.cc b/src/maglev/maglev-graph-builder.cc
index ffbf409ac9ae9675372068a57277c830488a3734..6b754b4d8592f0214f731dc9b298b3aab59527f2 100644
index 1c9ad68b298906cac8534a5373737643801eb567..d0796cd1a1bfd74b0ff506f8b1ff840bb6fc964b 100644
--- a/src/maglev/maglev-graph-builder.cc
+++ b/src/maglev/maglev-graph-builder.cc
@@ -1539,19 +1539,19 @@ DeoptFrame* MaglevGraphBuilder::GetCallerDeoptFrame() {

View File

@@ -8,16 +8,10 @@ const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const { chunkFilenames, findMatchingFiles } = require('./lib/utils');
const { chunkFilenames, findMatchingFiles, getDepotToolsEnv } = require('./lib/utils');
const ELECTRON_ROOT = path.normalize(path.dirname(__dirname));
const SOURCE_ROOT = path.resolve(ELECTRON_ROOT, '..');
const DEPOT_TOOLS = path.resolve(SOURCE_ROOT, 'third_party', 'depot_tools');
// Augment the PATH for this script so that we can find executables
// in the depot_tools folder even if folks do not have an instance of
// DEPOT_TOOLS in their path already
process.env.PATH = `${process.env.PATH}${path.delimiter}${DEPOT_TOOLS}`;
const IGNORELIST = new Set([
['shell', 'browser', 'resources', 'win', 'resource.h'],
@@ -93,7 +87,7 @@ async function runEslint (eslint, filenames, { fix, verbose }) {
function cpplint (args) {
args.unshift(`--root=${SOURCE_ROOT}`);
const cmd = IS_WINDOWS ? 'cpplint.bat' : 'cpplint.py';
const result = childProcess.spawnSync(cmd, args, { encoding: 'utf8', shell: true });
const result = childProcess.spawnSync(cmd, args, { encoding: 'utf8', shell: true, env: getDepotToolsEnv() });
// cpplint.py writes EVERYTHING to stderr, including status messages
if (result.stderr) {
for (const line of result.stderr.split(/[\r\n]+/)) {
@@ -118,6 +112,7 @@ const LINTERS = [{
test: filename => filename.endsWith('.cc') || (filename.endsWith('.h') && !isObjCHeader(filename)),
run: (opts, filenames) => {
const env = {
...getDepotToolsEnv(),
CHROMIUM_BUILDTOOLS_PATH: path.resolve(ELECTRON_ROOT, '..', 'buildtools')
};
const clangFormatFlags = opts.fix ? ['--fix'] : [];
@@ -132,6 +127,7 @@ const LINTERS = [{
test: filename => filename.endsWith('.mm') || (filename.endsWith('.h') && isObjCHeader(filename)),
run: (opts, filenames) => {
const env = {
...getDepotToolsEnv(),
CHROMIUM_BUILDTOOLS_PATH: path.resolve(ELECTRON_ROOT, '..', 'buildtools')
};
const clangFormatFlags = opts.fix ? ['--fix'] : [];
@@ -144,10 +140,8 @@ const LINTERS = [{
roots: ['script'],
test: filename => filename.endsWith('.py'),
run: (opts, filenames) => {
const rcfile = path.join(DEPOT_TOOLS, 'pylintrc-2.17');
const args = ['--rcfile=' + rcfile, ...filenames];
const env = { PYTHONPATH: path.join(ELECTRON_ROOT, 'script'), ...process.env };
spawnAndCheckExitCode(IS_WINDOWS ? 'pylint-2.17.bat' : 'pylint-2.17', args, { env });
const env = { ...getDepotToolsEnv(), PYTHONPATH: path.join(ELECTRON_ROOT, 'script') };
spawnAndCheckExitCode(IS_WINDOWS ? 'pylint-2.17.bat' : 'pylint-2.17', filenames, { env });
}
}, {
key: 'javascript',
@@ -176,9 +170,9 @@ const LINTERS = [{
run: (opts, filenames) => {
const allOk = filenames.map(filename => {
const env = {
...getDepotToolsEnv(),
CHROMIUM_BUILDTOOLS_PATH: path.resolve(ELECTRON_ROOT, '..', 'buildtools'),
DEPOT_TOOLS_WIN_TOOLCHAIN: '0',
...process.env
DEPOT_TOOLS_WIN_TOOLCHAIN: '0'
};
const args = ['format', filename];
if (!opts.fix) args.push('--dry-run');

View File

@@ -1,25 +0,0 @@
import os
import subprocess
import sys
from lib.util import get_depot_tools_env
SOURCE_ROOT = os.path.dirname(os.path.dirname(__file__))
# Helper to run gn format on multiple files
# (gn only formats a single file at a time)
def main():
new_env = get_depot_tools_env()
new_env['DEPOT_TOOLS_WIN_TOOLCHAIN'] = '0'
new_env['CHROMIUM_BUILDTOOLS_PATH'] = os.path.realpath(
os.path.join(SOURCE_ROOT, '..', 'buildtools')
)
for gn_file in sys.argv[1:]:
subprocess.check_call(
['gn', 'format', gn_file],
env=new_env
)
if __name__ == '__main__':
sys.exit(main())

View File

@@ -11,11 +11,14 @@
#include "base/command_line.h"
#include "base/containers/extend.h"
#include "base/files/file_util.h"
#include "base/no_destructor.h"
#include "base/strings/string_split.h"
#include "components/services/heap_profiling/public/cpp/profiling_client.h"
#include "content/public/common/buildflags.h"
#include "electron/buildflags/buildflags.h"
#include "electron/fuses.h"
#include "extensions/common/constants.h"
#include "mojo/public/cpp/bindings/binder_map.h"
#include "pdf/buildflags.h"
#include "shell/common/options_switches.h"
#include "shell/common/process_util.h"
@@ -227,4 +230,18 @@ bool ElectronContentClient::IsFilePickerAllowedForCrossOriginSubframe(
#endif
}
void ElectronContentClient::ExposeInterfacesToBrowser(
scoped_refptr<base::SequencedTaskRunner> io_task_runner,
mojo::BinderMap* binders) {
// Sets up the client side of the multi-process heap profiler service.
binders->Add<heap_profiling::mojom::ProfilingClient>(
[](mojo::PendingReceiver<heap_profiling::mojom::ProfilingClient>
receiver) {
static base::NoDestructor<heap_profiling::ProfilingClient>
profiling_client;
profiling_client->BindToInterface(std::move(receiver));
},
io_task_runner);
}
} // namespace electron

View File

@@ -36,6 +36,9 @@ class ElectronContentClient : public content::ContentClient {
std::vector<media::CdmHostFilePath>* cdm_host_file_paths) override;
bool IsFilePickerAllowedForCrossOriginSubframe(
const url::Origin& origin) override;
void ExposeInterfacesToBrowser(
scoped_refptr<base::SequencedTaskRunner> io_task_runner,
mojo::BinderMap* binders) override;
};
} // namespace electron

View File

@@ -25,7 +25,10 @@
#include "base/strings/string_util_internal.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/profiler/process_type.h"
#include "components/content_settings/core/common/content_settings_pattern.h"
#include "components/memory_system/initializer.h"
#include "components/memory_system/parameters.h"
#include "content/public/app/initialize_mojo_core.h"
#include "content/public/common/content_switches.h"
#include "crypto/hash.h"
@@ -359,6 +362,31 @@ std::optional<int> ElectronMainDelegate::PreBrowserMain() {
return std::nullopt;
}
std::optional<int> ElectronMainDelegate::PostEarlyInitialization(
InvokedIn invoked_in) {
// Start memory observation as early as possible so it can start recording
// memory allocations.
InitializeMemorySystem();
return std::nullopt;
}
void ElectronMainDelegate::InitializeMemorySystem() {
const base::CommandLine* const command_line =
base::CommandLine::ForCurrentProcess();
const std::string process_type =
command_line->GetSwitchValueASCII(::switches::kProcessType);
// PoissonAllocationSampler is necessary for heap profiling.
memory_system::Initializer()
.SetDispatcherParameters(memory_system::DispatcherParameters::
PoissonAllocationSamplerInclusion::kEnforce,
memory_system::DispatcherParameters::
AllocationTraceRecorderInclusion::kIgnore,
process_type)
.Initialize(memory_system_);
}
std::string_view ElectronMainDelegate::GetBrowserV8SnapshotFilename() {
bool load_browser_process_specific_v8_snapshot =
IsBrowserProcess() &&

View File

@@ -9,6 +9,7 @@
#include <string>
#include <string_view>
#include "components/memory_system/memory_system.h"
#include "content/public/app/content_main_delegate.h"
namespace content {
@@ -47,6 +48,7 @@ class ElectronMainDelegate : public content::ContentMainDelegate {
void PreSandboxStartup() override;
void SandboxInitialized(const std::string& process_type) override;
std::optional<int> PreBrowserMain() override;
std::optional<int> PostEarlyInitialization(InvokedIn invoked_in) override;
content::ContentClient* CreateContentClient() override;
content::ContentBrowserClient* CreateContentBrowserClient() override;
content::ContentGpuClient* CreateContentGpuClient() override;
@@ -63,6 +65,8 @@ class ElectronMainDelegate : public content::ContentMainDelegate {
void ZygoteForked() override;
#endif
void InitializeMemorySystem();
private:
std::unique_ptr<content::ContentBrowserClient> browser_client_;
std::unique_ptr<content::ContentClient> content_client_;
@@ -70,6 +74,8 @@ class ElectronMainDelegate : public content::ContentMainDelegate {
std::unique_ptr<content::ContentRendererClient> renderer_client_;
std::unique_ptr<content::ContentUtilityClient> utility_client_;
std::unique_ptr<tracing::TracingSamplerProfiler> tracing_sampler_profiler_;
memory_system::MemorySystem memory_system_;
};
} // namespace electron

View File

@@ -12,7 +12,13 @@
#include "base/task/thread_pool.h"
#include "base/threading/thread_restrictions.h"
#include "base/trace_event/trace_config.h"
#if !defined(ADDRESS_SANITIZER)
#include "components/heap_profiling/multi_process/client_connection_manager.h"
#include "components/heap_profiling/multi_process/supervisor.h"
#include "components/services/heap_profiling/public/cpp/settings.h"
#endif // !defined(ADDRESS_SANITIZER)
#include "content/public/browser/tracing_controller.h"
#include "shell/browser/browser.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/gin_converters/callback_converter.h"
#include "shell/common/gin_converters/file_path_converter.h"
@@ -102,6 +108,12 @@ v8::Local<v8::Promise> StopRecording(gin::Arguments* const args) {
gin_helper::Promise<base::FilePath> promise{args->isolate()};
v8::Local<v8::Promise> handle = promise.GetHandle();
if (!electron::Browser::Get()->is_ready()) {
promise.RejectWithErrorMessage(
"contentTracing cannot be used before app is ready");
return handle;
}
base::FilePath path;
if (args->GetNext(&path) && !path.empty()) {
StopTracing(std::move(promise), std::make_optional(path));
@@ -120,6 +132,12 @@ v8::Local<v8::Promise> GetCategories(v8::Isolate* isolate) {
gin_helper::Promise<const std::set<std::string>&> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
if (!electron::Browser::Get()->is_ready()) {
promise.RejectWithErrorMessage(
"contentTracing cannot be used before app is ready");
return handle;
}
// Note: This method always succeeds.
TracingController::GetInstance()->GetCategories(base::BindOnce(
gin_helper::Promise<const std::set<std::string>&>::ResolvePromise,
@@ -128,12 +146,98 @@ v8::Local<v8::Promise> GetCategories(v8::Isolate* isolate) {
return handle;
}
#if !defined(ADDRESS_SANITIZER)
std::tuple<heap_profiling::Mode, heap_profiling::mojom::StackMode, uint32_t>
GetHeapProfilingOptions(gin::Arguments* const args) {
heap_profiling::Mode mode = heap_profiling::Mode::kAll;
heap_profiling::mojom::StackMode stack_mode =
heap_profiling::mojom::StackMode::NATIVE_WITHOUT_THREAD_NAMES;
uint32_t sampling_rate = 100000;
gin_helper::Dictionary options;
if (args->GetNext(&options)) {
std::string mode_in;
std::string stack_mode_in;
std::optional<uint32_t> sampling_rate_in;
if (options.Get("mode", &mode_in)) {
heap_profiling::Mode converted =
heap_profiling::ConvertStringToMode(mode_in);
if (converted != heap_profiling::Mode::kNone &&
converted != heap_profiling::Mode::kManual) {
mode = converted;
}
}
if (options.Get("stackMode", &stack_mode_in)) {
stack_mode = heap_profiling::ConvertStringToStackMode(stack_mode_in);
}
if (options.GetOptional("samplingRate", &sampling_rate_in) &&
sampling_rate_in && sampling_rate_in.value() >= 1000 &&
sampling_rate_in.value() <= 10000000) {
sampling_rate = sampling_rate_in.value();
}
}
return {mode, stack_mode, sampling_rate};
}
bool g_heap_profiling_started = false;
#endif // !defined(ADDRESS_SANITIZER)
v8::Local<v8::Promise> EnableHeapProfiling(gin::Arguments* const args) {
#if defined(ADDRESS_SANITIZER)
// Memory sanitizers are using large memory shadow to keep track of memory
// state. Using memlog and memory sanitizers at the same time is slowing down
// user experience, causing the browser to be barely responsive. In theory,
// memlog and memory sanitizers are compatible and can run at the same time.
return gin_helper::Promise<void>::ResolvedPromise(args->isolate());
#else
gin_helper::Promise<void> promise(args->isolate());
v8::Local<v8::Promise> handle = promise.GetHandle();
auto* supervisor = heap_profiling::Supervisor::GetInstance();
if (supervisor->HasStarted() || g_heap_profiling_started) {
promise.RejectWithErrorMessage("Heap profiling is already enabled");
return handle;
}
// HasStarted() becomes true asynchronously. We keep track of whether we have
// called Start() already to avoid calling Start() twice.
g_heap_profiling_started = true;
auto [mode, stack_mode, sampling_rate] = GetHeapProfilingOptions(args);
supervisor->SetClientConnectionManagerConstructor(
[](base::WeakPtr<heap_profiling::Controller> controller_weak_ptr,
heap_profiling::Mode mode) {
return std::make_unique<heap_profiling::ClientConnectionManager>(
controller_weak_ptr, mode);
});
supervisor->Start(mode, stack_mode, sampling_rate,
base::BindOnce(gin_helper::Promise<void>::ResolvePromise,
std::move(promise)));
return handle;
#endif // defined(ADDRESS_SANITIZER)
}
v8::Local<v8::Promise> StartTracing(
v8::Isolate* isolate,
const base::trace_event::TraceConfig& trace_config) {
gin_helper::Promise<void> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
if (!electron::Browser::Get()->is_ready()) {
promise.RejectWithErrorMessage(
"contentTracing cannot be used before app is ready");
return handle;
}
if (!TracingController::GetInstance()->StartTracing(
trace_config,
base::BindOnce(gin_helper::Promise<void>::ResolvePromise,
@@ -165,6 +269,12 @@ v8::Local<v8::Promise> GetTraceBufferUsage(v8::Isolate* isolate) {
gin_helper::Promise<gin_helper::Dictionary> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
if (!electron::Browser::Get()->is_ready()) {
promise.RejectWithErrorMessage(
"contentTracing cannot be used before app is ready");
return handle;
}
// Note: This method always succeeds.
TracingController::GetInstance()->GetTraceBufferUsage(
base::BindOnce(&OnTraceBufferUsageAvailable, std::move(promise)));
@@ -181,6 +291,7 @@ void Initialize(v8::Local<v8::Object> exports,
dict.SetMethod("startRecording", &StartTracing);
dict.SetMethod("stopRecording", &StopRecording);
dict.SetMethod("getTraceBufferUsage", &GetTraceBufferUsage);
dict.SetMethod("enableHeapProfiling", &EnableHeapProfiling);
}
} // namespace

View File

@@ -459,6 +459,8 @@ constexpr char kFooterTemplate[] = "footerTemplate";
constexpr char kPreferCSSPageSize[] = "preferCSSPageSize";
constexpr char kGenerateTaggedPDF[] = "generateTaggedPDF";
constexpr char kGenerateDocumentOutline[] = "generateDocumentOutline";
constexpr char kDpiHorizontal[] = "horizontal";
constexpr char kDpiVertical[] = "vertical";
#endif // BUILDFLAG(ENABLE_PRINTING)
constexpr std::string_view CursorTypeToString(
@@ -885,6 +887,8 @@ WebContents::WebContents(v8::Isolate* isolate,
&offscreen_use_shared_texture_);
use_offscreen_dict.Get(options::kSharedTexturePixelFormat,
&offscreen_shared_texture_pixel_format_);
use_offscreen_dict.Get(options::kDeviceScaleFactor,
&offscreen_device_scale_factor_);
}
}
@@ -923,6 +927,7 @@ WebContents::WebContents(v8::Isolate* isolate,
auto* view = new OffScreenWebContentsView(
false, offscreen_use_shared_texture_,
offscreen_shared_texture_pixel_format_,
offscreen_device_scale_factor_,
base::BindRepeating(&WebContents::OnPaint, base::Unretained(this)));
params.view = view;
params.delegate_view = view;
@@ -944,7 +949,7 @@ WebContents::WebContents(v8::Isolate* isolate,
content::WebContents::CreateParams params(session->browser_context());
auto* view = new OffScreenWebContentsView(
transparent, offscreen_use_shared_texture_,
offscreen_shared_texture_pixel_format_,
offscreen_shared_texture_pixel_format_, offscreen_device_scale_factor_,
base::BindRepeating(&WebContents::OnPaint, base::Unretained(this)));
params.view = view;
params.delegate_view = view;
@@ -1318,7 +1323,8 @@ void WebContents::MaybeOverrideCreateParamsForNewWindow(
// to the child WebContents in AddNewContents via SetCallback().
auto* view = new OffScreenWebContentsView(
false, offscreen_use_shared_texture_,
offscreen_shared_texture_pixel_format_, base::DoNothing());
offscreen_shared_texture_pixel_format_,
offscreen_device_scale_factor_, base::DoNothing());
create_params->view = view;
create_params->delegate_view = view;
}
@@ -3342,10 +3348,16 @@ void WebContents::Print(gin::Arguments* const args) {
// Set custom dots per inch (dpi)
if (gin_helper::Dictionary dpi; options.Get(kDpi, &dpi)) {
// `webContents.print()` exposes `dpi: { horizontal, vertical }` in JS.
// Keep backward compatibility with internal key names as a fallback.
settings.Set(printing::kSettingDpiHorizontal,
dpi.ValueOrDefault(printing::kSettingDpiHorizontal, 72));
dpi.ValueOrDefault(
kDpiHorizontal,
dpi.ValueOrDefault(printing::kSettingDpiHorizontal, 72)));
settings.Set(printing::kSettingDpiVertical,
dpi.ValueOrDefault(printing::kSettingDpiVertical, 72));
dpi.ValueOrDefault(
kDpiVertical,
dpi.ValueOrDefault(printing::kSettingDpiVertical, 72)));
}
print_task_runner_->PostTaskAndReplyWithResult(

View File

@@ -826,6 +826,13 @@ class WebContents final : public ExclusiveAccessContext,
bool offscreen_use_shared_texture_ = false;
std::string offscreen_shared_texture_pixel_format_ = "argb";
// TODO(reito): 0.0f means the device scale factor is not set, it's a
// migration of the breaking change so that we can read the device scale
// factor from physical primary screen's info. In Electron 42, we need to set
// this to 1.0f so that the offscreen rendering use 1.0 as default when
// `deviceScaleFactor` is not specified in webPreferences.
float offscreen_device_scale_factor_ = 0.0f;
// Whether window is fullscreened by HTML5 api.
bool html_fullscreen_ = false;

View File

@@ -4,6 +4,7 @@
#include "shell/browser/net/electron_url_loader_factory.h"
#include <algorithm>
#include <memory>
#include <string>
#include <string_view>
@@ -26,11 +27,15 @@
#include "net/http/http_status_code.h"
#include "net/http/http_util.h"
#include "net/url_request/redirect_util.h"
#include "services/network/public/cpp/cors/cors.h"
#include "services/network/public/cpp/cors/cors_error_status.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/cpp/simple_url_loader_stream_consumer.h"
#include "services/network/public/cpp/url_loader_completion_status.h"
#include "services/network/public/mojom/cors.mojom.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
@@ -46,6 +51,7 @@
#include "shell/common/gin_helper/dictionary.h"
#include "third_party/abseil-cpp/absl/strings/str_format.h"
#include "third_party/blink/public/mojom/loader/resource_load_info.mojom-shared.h"
#include "url/url_util.h"
#include "shell/common/node_includes.h"
@@ -422,6 +428,26 @@ void ElectronURLLoaderFactory::CreateLoaderAndStart(
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Subresource requests for registered protocols reach this factory via the
// renderer's per-scheme URLLoaderFactoryBundle entry, which bypasses the
// network service's CorsURLLoaderFactory entirely. Replicate the
// kCorsDisabledScheme gate from CorsURLLoader::StartRequest so a cross-origin
// page cannot read responses from a scheme registered with
// {supportFetchAPI: true} but without {corsEnabled: true}. Browser-initiated
// requests (no |request_initiator|) are trusted and skipped.
if (request.request_initiator &&
network::cors::ShouldCheckCors(request.url, request.request_initiator,
request.mode) &&
!std::ranges::contains(url::GetCorsEnabledSchemes(),
request.url.GetScheme())) {
mojo::Remote<network::mojom::URLLoaderClient> client_remote(
std::move(client));
client_remote->OnComplete(
network::URLLoaderCompletionStatus(network::CorsErrorStatus(
network::mojom::CorsError::kCorsDisabledScheme)));
return;
}
// |StartLoading| is used for both intercepted and registered protocols,
// and on redirects it needs a factory to use to create a loader for the
// new request. So in this case, this factory is the target factory.
@@ -482,6 +508,16 @@ void ElectronURLLoaderFactory::StartLoading(
network::mojom::URLResponseHeadPtr head = ToResponseHead(dict);
// For cross-origin no-cors loads (e.g. <img>, fetch({mode:'no-cors'})), the
// body must not be script-readable; tag the response as opaque so Blink
// applies opaque filtering. CorsURLLoader normally does this, but per-scheme
// factories bypass it.
if (request.mode == network::mojom::RequestMode::kNoCors &&
request.request_initiator &&
!request.request_initiator->IsSameOriginWith(request.url)) {
head->response_type = network::mojom::FetchResponseType::kOpaque;
}
// Handle redirection.
//
// Note that with NetworkService, sending the "Location" header no longer

View File

@@ -13,6 +13,7 @@
#include "base/strings/string_split.h"
#include "content/public/browser/browser_context.h"
#include "extensions/browser/extension_navigation_ui_data.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "net/base/completion_repeating_callback.h"
#include "net/base/load_flags.h"
#include "net/http/http_response_headers.h"
@@ -29,6 +30,30 @@
namespace electron {
namespace {
class NoOpHeaderClient final : public network::mojom::TrustedHeaderClient {
public:
NoOpHeaderClient() = default;
NoOpHeaderClient(const NoOpHeaderClient&) = delete;
NoOpHeaderClient& operator=(const NoOpHeaderClient&) = delete;
~NoOpHeaderClient() override = default;
void OnBeforeSendHeaders(const net::HttpRequestHeaders& headers,
OnBeforeSendHeadersCallback callback) override {
std::move(callback).Run(net::OK, std::nullopt);
}
void OnHeadersReceived(const std::string& headers,
const net::IPEndPoint& remote_endpoint,
const std::optional<net::SSLInfo>& ssl_info,
OnHeadersReceivedCallback callback) override {
std::move(callback).Run(net::OK, std::nullopt, std::nullopt);
}
};
} // namespace
ProxyingURLLoaderFactory::InProgressRequest::FollowRedirectParams::
FollowRedirectParams() = default;
ProxyingURLLoaderFactory::InProgressRequest::FollowRedirectParams::
@@ -863,8 +888,14 @@ void ProxyingURLLoaderFactory::OnLoaderCreated(
int32_t request_id,
mojo::PendingReceiver<network::mojom::TrustedHeaderClient> receiver) {
auto it = network_request_id_to_web_request_id_.find(request_id);
if (it == network_request_id_to_web_request_id_.end())
if (it == network_request_id_to_web_request_id_.end()) {
// Chromium can require the header client pipe to be bound even when
// Electron is using the pass-through path. Dropping the receiver here
// disconnects the URLLoader and causes the request to fail with ERR_FAILED.
mojo::MakeSelfOwnedReceiver(std::make_unique<NoOpHeaderClient>(),
std::move(receiver));
return;
}
auto request_it = requests_.find(it->second);
DCHECK(request_it != requests_.end());

View File

@@ -27,6 +27,7 @@
#include "content/public/browser/browser_thread.h"
#include "shell/browser/notifications/notification_delegate.h"
#include "shell/browser/notifications/win/notification_presenter_win.h"
#include "shell/browser/notifications/win/windows_toast_activator.h"
#include "shell/browser/win/scoped_hstring.h"
#include "shell/common/application_info.h"
#include "third_party/libxml/chromium/xml_writer.h"
@@ -35,6 +36,12 @@
using ABI::Windows::Data::Xml::Dom::IXmlDocument;
using ABI::Windows::Data::Xml::Dom::IXmlDocumentIO;
using ABI::Windows::Foundation::IPropertyValue;
using ABI::Windows::Foundation::Collections::IIterable;
using ABI::Windows::Foundation::Collections::IIterator;
using ABI::Windows::Foundation::Collections::IKeyValuePair;
using ABI::Windows::Foundation::Collections::IMap;
using ABI::Windows::Foundation::Collections::IPropertySet;
using Microsoft::WRL::Wrappers::HStringReference;
namespace winui = ABI::Windows::UI;
@@ -758,19 +765,86 @@ ToastEventHandler::ToastEventHandler(Notification* notification)
ToastEventHandler::~ToastEventHandler() = default;
namespace {
// Extracts string user-input values from an IToastActivatedEventArgs2.
// Windows only fires the WinRT Activated event (not the COM activator) for
// `activationType="foreground"` actions while the app is already running, so
// reply text and selection values must be pulled from UserInput here rather
// than from the COM callback's NOTIFICATION_USER_INPUT_DATA.
std::vector<ActivationUserInput> ExtractUserInputs(IInspectable* args) {
std::vector<ActivationUserInput> inputs;
if (!args)
return inputs;
ComPtr<winui::Notifications::IToastActivatedEventArgs2> args2;
if (FAILED(args->QueryInterface(IID_PPV_ARGS(&args2))) || !args2)
return inputs;
ComPtr<IPropertySet> user_input;
if (FAILED(args2->get_UserInput(&user_input)) || !user_input)
return inputs;
ComPtr<IMap<HSTRING, IInspectable*>> map;
if (FAILED(user_input.As(&map)) || !map)
return inputs;
ComPtr<IIterable<IKeyValuePair<HSTRING, IInspectable*>*>> iterable;
if (FAILED(map.As(&iterable)) || !iterable)
return inputs;
ComPtr<IIterator<IKeyValuePair<HSTRING, IInspectable*>*>> iter;
if (FAILED(iterable->First(&iter)) || !iter)
return inputs;
boolean has_current = false;
if (FAILED(iter->get_HasCurrent(&has_current)))
return inputs;
while (has_current) {
ComPtr<IKeyValuePair<HSTRING, IInspectable*>> kvp;
if (FAILED(iter->get_Current(&kvp)) || !kvp)
break;
ScopedHString key_hs;
ComPtr<IInspectable> value;
if (SUCCEEDED(kvp->get_Key(key_hs.Receive())) &&
SUCCEEDED(kvp->get_Value(&value)) && key_hs.success() && value) {
ComPtr<IPropertyValue> prop;
ScopedHString value_hs;
if (SUCCEEDED(value.As(&prop)) && prop &&
SUCCEEDED(prop->GetString(value_hs.Receive())) &&
value_hs.success()) {
UINT32 key_len = 0;
UINT32 val_len = 0;
const wchar_t* key_raw = WindowsGetStringRawBuffer(key_hs, &key_len);
const wchar_t* val_raw = WindowsGetStringRawBuffer(value_hs, &val_len);
ActivationUserInput ui;
if (key_raw && key_len)
ui.key.assign(key_raw, key_len);
if (val_raw && val_len)
ui.value.assign(val_raw, val_len);
inputs.push_back(std::move(ui));
}
}
if (FAILED(iter->MoveNext(&has_current)))
break;
}
return inputs;
}
} // namespace
IFACEMETHODIMP ToastEventHandler::Invoke(
winui::Notifications::IToastNotification* sender,
IInspectable* args) {
std::wstring arguments_w;
std::wstring tag_w;
std::wstring group_w;
if (args) {
Microsoft::WRL::ComPtr<winui::Notifications::IToastActivatedEventArgs>
activated_args;
ComPtr<winui::Notifications::IToastActivatedEventArgs> activated_args;
if (SUCCEEDED(args->QueryInterface(IID_PPV_ARGS(&activated_args)))) {
HSTRING args_hs = nullptr;
if (SUCCEEDED(activated_args->get_Arguments(&args_hs)) && args_hs) {
ScopedHString args_hs;
if (SUCCEEDED(activated_args->get_Arguments(args_hs.Receive())) &&
args_hs.success()) {
UINT32 len = 0;
const wchar_t* raw = WindowsGetStringRawBuffer(args_hs, &len);
if (raw && len)
@@ -779,38 +853,24 @@ IFACEMETHODIMP ToastEventHandler::Invoke(
}
}
if (sender) {
Microsoft::WRL::ComPtr<winui::Notifications::IToastNotification2> toast2;
if (SUCCEEDED(sender->QueryInterface(IID_PPV_ARGS(&toast2)))) {
HSTRING tag_hs = nullptr;
if (SUCCEEDED(toast2->get_Tag(&tag_hs)) && tag_hs) {
UINT32 len = 0;
const wchar_t* raw = WindowsGetStringRawBuffer(tag_hs, &len);
if (raw && len)
tag_w.assign(raw, len);
}
HSTRING group_hs = nullptr;
if (SUCCEEDED(toast2->get_Group(&group_hs)) && group_hs) {
UINT32 len = 0;
const wchar_t* raw = WindowsGetStringRawBuffer(group_hs, &len);
if (raw && len)
group_w.assign(raw, len);
}
}
}
std::string notif_id;
std::string notif_hash;
if (notification_) {
notif_id = notification_->notification_id();
notif_hash = base::NumberToString(base::FastHash(notif_id));
}
bool structured = arguments_w.find(L"&tag=") != std::wstring::npos ||
arguments_w.find(L"type=action") != std::wstring::npos ||
arguments_w.find(L"type=reply") != std::wstring::npos;
if (structured)
// For structured action/reply args, dispatch through the same handler the
// COM activator uses. Previously this path early-returned, assuming the COM
// INotificationActivationCallback would fire — but for non-MSIX apps with
// activationType="foreground" (and for MSIX apps while already running)
// Windows only raises the in-process WinRT Activated event, so those
// action/reply events were being silently dropped. See electron/electron
// issue #51147.
const bool structured =
arguments_w.find(L"type=action") != std::wstring::npos ||
arguments_w.find(L"type=reply") != std::wstring::npos;
if (structured) {
std::vector<ActivationUserInput> inputs = ExtractUserInputs(args);
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&HandleToastActivation, arguments_w, std::move(inputs)));
DebugLog("Notification activated (structured)");
return S_OK;
}
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,

View File

@@ -52,8 +52,6 @@ namespace electron {
namespace {
const float kDefaultScaleFactor = 1.0;
ui::MouseEvent UiMouseEventFromWebMouseEvent(blink::WebMouseEvent event) {
int button_flags = 0;
switch (event.button) {
@@ -95,6 +93,15 @@ ui::MouseWheelEvent UiMouseWheelEventFromWebMouseEvent(
base::ClampFloor<int>(event.delta_y)};
}
// TODO(reito): Remove this function and use default 1.0f when Electron 42.
float GetDefaultDeviceScaleFactorFromDisplayInfo() {
display::Display display =
display::Screen::Get()->GetDisplayNearestView(gfx::NativeView());
const float factor = display.device_scale_factor();
return factor > 0 ? factor : 1.0f;
}
} // namespace
class ElectronDelegatedFrameHostClient
@@ -154,6 +161,7 @@ OffScreenRenderWidgetHostView::OffScreenRenderWidgetHostView(
bool transparent,
bool offscreen_use_shared_texture,
const std::string& offscreen_shared_texture_pixel_format,
float offscreen_device_scale_factor,
bool painting,
int frame_rate,
const OnPaintCallback& callback,
@@ -167,6 +175,7 @@ OffScreenRenderWidgetHostView::OffScreenRenderWidgetHostView(
offscreen_use_shared_texture_(offscreen_use_shared_texture),
offscreen_shared_texture_pixel_format_(
offscreen_shared_texture_pixel_format),
offscreen_device_scale_factor_(offscreen_device_scale_factor),
callback_(callback),
frame_rate_(frame_rate),
size_(initial_size),
@@ -183,11 +192,11 @@ OffScreenRenderWidgetHostView::OffScreenRenderWidgetHostView(
DCHECK(render_widget_host_);
DCHECK(!render_widget_host_->GetView());
// Initialize a screen_infos_ struct as needed, to cache the scale factor.
if (screen_infos_.screen_infos.empty()) {
UpdateScreenInfo();
// TODO(reito): Remove this when Electron 42.
if (cc::MathUtil::IsWithinEpsilon(offscreen_device_scale_factor_, 0.0f)) {
offscreen_device_scale_factor_ =
GetDefaultDeviceScaleFactorFromDisplayInfo();
}
screen_infos_.mutable_current().device_scale_factor = kDefaultScaleFactor;
delegated_frame_host_allocator_.GenerateId();
delegated_frame_host_surface_id_ =
@@ -209,15 +218,6 @@ OffScreenRenderWidgetHostView::OffScreenRenderWidgetHostView(
compositor_->SetDelegate(this);
compositor_->SetRootLayer(root_layer_.get());
// For offscreen rendering with format rgbaf16, we need to set correct display
// color spaces to the compositor, otherwise it won't support hdr.
if (offscreen_use_shared_texture_ &&
offscreen_shared_texture_pixel_format_ == "rgbaf16") {
gfx::DisplayColorSpaces hdr_display_color_spaces(
gfx::ColorSpace::CreateSRGBLinear(), viz::SinglePlaneFormat::kRGBA_F16);
compositor_->SetDisplayColorSpaces(hdr_display_color_spaces);
}
ResizeRootLayer(false);
render_widget_host_->SetView(this);
@@ -503,19 +503,6 @@ void OffScreenRenderWidgetHostView::CopyFromSurface(
src_rect, output_size, base::TimeDelta(), std::move(callback));
}
display::ScreenInfo OffScreenRenderWidgetHostView::GetScreenInfo() const {
display::ScreenInfo screen_info;
screen_info.depth = 24;
screen_info.depth_per_component = 8;
screen_info.orientation_angle = 0;
screen_info.device_scale_factor = GetDeviceScaleFactor();
screen_info.orientation_type =
display::mojom::ScreenOrientation::kLandscapePrimary;
screen_info.rect = gfx::Rect(size_);
screen_info.available_rect = gfx::Rect(size_);
return screen_info;
}
gfx::Rect OffScreenRenderWidgetHostView::GetBoundsInRootWindow() {
return gfx::Rect(size_);
}
@@ -561,8 +548,8 @@ OffScreenRenderWidgetHostView::CreateViewForWidget(
return new OffScreenRenderWidgetHostView(
transparent_, offscreen_use_shared_texture_,
offscreen_shared_texture_pixel_format_, true,
embedder_host_view->frame_rate(), callback_, render_widget_host,
offscreen_shared_texture_pixel_format_, offscreen_device_scale_factor_,
true, embedder_host_view->frame_rate(), callback_, render_widget_host,
embedder_host_view, size());
}
@@ -970,35 +957,55 @@ void OffScreenRenderWidgetHostView::InvalidateBounds(const gfx::Rect& bounds) {
CompositeFrame(bounds);
}
display::ScreenInfos
OffScreenRenderWidgetHostView::GetNewScreenInfosForUpdate() {
display::ScreenInfo screen_info;
screen_info.depth = 24;
screen_info.depth_per_component = 8;
screen_info.orientation_angle = 0;
screen_info.orientation_type =
display::mojom::ScreenOrientation::kLandscapePrimary;
screen_info.rect = gfx::Rect(size_);
screen_info.available_rect = gfx::Rect(size_);
screen_info.device_scale_factor = offscreen_device_scale_factor_;
// When pixel format is 'rgbaf16', we need to set screen info to support HDR.
if (offscreen_use_shared_texture_ &&
offscreen_shared_texture_pixel_format_ == "rgbaf16") {
gfx::DisplayColorSpaces hdr_display_color_spaces{
gfx::ColorSpace::CreateSRGBLinear(), viz::SinglePlaneFormat::kRGBA_F16};
// The max luminance value doesn't matter so we set to a large value.
hdr_display_color_spaces.SetHDRMaxLuminanceRelative(100.0f);
screen_info.display_color_spaces = hdr_display_color_spaces;
}
display::ScreenInfos screen_infos{screen_info};
return screen_infos;
}
void OffScreenRenderWidgetHostView::ResizeRootLayer(bool force) {
SetupFrameRate(false);
display::Display display =
display::Screen::Get()->GetDisplayNearestView(GetNativeView());
const float scaleFactor = display.device_scale_factor();
float sf = GetDeviceScaleFactor();
const bool sf_did_change = scaleFactor != sf;
// Initialize a screen_infos_ struct as needed, to cache the scale factor.
if (screen_infos_.screen_infos.empty()) {
UpdateScreenInfo();
}
screen_infos_.mutable_current().device_scale_factor = scaleFactor;
auto old_screen_info = screen_infos_.current();
UpdateScreenInfo();
auto new_screen_info = screen_infos_.current();
gfx::Size size = GetViewBounds().size();
if (!force && !sf_did_change && size == root_layer()->bounds().size())
if (!force && size == root_layer()->bounds().size() &&
old_screen_info == new_screen_info)
return;
root_layer()->SetBounds(gfx::Rect(size));
const gfx::Size& size_in_pixels =
gfx::ToFlooredSize(gfx::ConvertSizeToPixels(size, sf));
auto sf = GetDeviceScaleFactor();
const gfx::Size& size_in_pixels = SizeInPixels();
if (compositor_) {
compositor_allocator_.GenerateId();
compositor_surface_id_ = compositor_allocator_.GetCurrentLocalSurfaceId();
compositor_->SetScaleAndSize(sf, size_in_pixels, compositor_surface_id_);
compositor_->SetDisplayColorSpaces(new_screen_info.display_color_spaces);
}
delegated_frame_host_allocator_.GenerateId();

View File

@@ -73,6 +73,7 @@ class OffScreenRenderWidgetHostView
bool transparent,
bool offscreen_use_shared_texture,
const std::string& offscreen_shared_texture_pixel_format,
float offscreen_device_scale_factor,
bool painting,
int frame_rate,
const OnPaintCallback& callback,
@@ -151,7 +152,6 @@ class OffScreenRenderWidgetHostView
base::TimeDelta timeout,
base::OnceCallback<void(const content::CopyFromSurfaceResult&)> callback)
override;
display::ScreenInfo GetScreenInfo() const override;
void TransformPointToRootSurface(gfx::PointF* point) override {}
gfx::Rect GetBoundsInRootWindow() override;
std::optional<content::DisplayFeature> GetDisplayFeature() override;
@@ -171,6 +171,7 @@ class OffScreenRenderWidgetHostView
const std::optional<std::vector<gfx::Rect>>& character_bounds) override {}
gfx::Size GetCompositorViewportPixelSize() override;
ui::Compositor* GetCompositor() override;
display::ScreenInfos GetNewScreenInfosForUpdate() override;
content::RenderWidgetHostViewBase* CreateViewForWidget(
content::RenderWidgetHost*,
@@ -293,6 +294,8 @@ class OffScreenRenderWidgetHostView
const bool transparent_;
const bool offscreen_use_shared_texture_;
const std::string offscreen_shared_texture_pixel_format_;
float offscreen_device_scale_factor_;
OnPaintCallback callback_;
OnPopupPaintCallback parent_callback_;

View File

@@ -17,11 +17,13 @@ OffScreenWebContentsView::OffScreenWebContentsView(
bool transparent,
bool offscreen_use_shared_texture,
const std::string& offscreen_shared_texture_pixel_format,
float offscreen_device_scale_factor,
const OnPaintCallback& callback)
: transparent_(transparent),
offscreen_use_shared_texture_(offscreen_use_shared_texture),
offscreen_shared_texture_pixel_format_(
offscreen_shared_texture_pixel_format),
offscreen_device_scale_factor_(offscreen_device_scale_factor),
callback_(callback) {
#if BUILDFLAG(IS_MAC)
PlatformCreate();
@@ -120,8 +122,9 @@ OffScreenWebContentsView::CreateViewForWidget(
return new OffScreenRenderWidgetHostView(
transparent_, offscreen_use_shared_texture_,
offscreen_shared_texture_pixel_format_, painting_, GetFrameRate(),
callback_, render_widget_host, nullptr, GetSize());
offscreen_shared_texture_pixel_format_, offscreen_device_scale_factor_,
painting_, GetFrameRate(), callback_, render_widget_host, nullptr,
GetSize());
}
content::RenderWidgetHostViewBase*
@@ -141,9 +144,9 @@ OffScreenWebContentsView::CreateViewForChildWidget(
return new OffScreenRenderWidgetHostView(
transparent_, offscreen_use_shared_texture_,
offscreen_shared_texture_pixel_format_, painting_,
embedder_host_view->frame_rate(), callback_, render_widget_host,
embedder_host_view, GetSize());
offscreen_shared_texture_pixel_format_, offscreen_device_scale_factor_,
painting_, embedder_host_view->frame_rate(), callback_,
render_widget_host, embedder_host_view, GetSize());
}
void OffScreenWebContentsView::RenderViewReady() {

View File

@@ -38,6 +38,7 @@ class OffScreenWebContentsView : public content::WebContentsView,
bool transparent,
bool offscreen_use_shared_texture,
const std::string& offscreen_shared_texture_pixel_format,
float offscreen_device_scale_factor,
const OnPaintCallback& callback);
~OffScreenWebContentsView() override;
@@ -114,6 +115,7 @@ class OffScreenWebContentsView : public content::WebContentsView,
const bool transparent_;
const bool offscreen_use_shared_texture_;
const std::string offscreen_shared_texture_pixel_format_;
const float offscreen_device_scale_factor_;
bool painting_ = true;
int frame_rate_ = 60;
OnPaintCallback callback_;

View File

@@ -172,20 +172,30 @@ void AutofillPopup::CreateView(content::RenderFrameHost* frame_host,
bool offscreen,
views::View* parent,
const gfx::RectF& r) {
DCHECK(parent);
Hide();
// A Widget can outlive its NativeWidget during teardown.
// Do not create a child popup once the parent native view is gone.
views::Widget* parent_widget = parent->GetWidget();
if (!parent_widget || parent_widget->IsClosed() ||
!parent_widget->GetNativeView()) {
return;
}
frame_host_ = frame_host;
element_bounds_ = gfx::ToEnclosedRect(r);
gfx::Vector2d height_offset(0, element_bounds_.height());
gfx::Point menu_position(element_bounds_.origin() + height_offset);
gfx::Vector2d height_offset{0, element_bounds_.height()};
gfx::Point menu_position{element_bounds_.origin() + height_offset};
views::View::ConvertPointToScreen(parent, &menu_position);
popup_bounds_ = gfx::Rect(menu_position, element_bounds_.size());
popup_bounds_ = gfx::Rect{menu_position, element_bounds_.size()};
parent_ = parent;
parent_->AddObserver(this);
view_ = new AutofillPopupView(this, parent->GetWidget());
view_ = new AutofillPopupView{this, parent_widget};
if (offscreen) {
auto* rwhv = embedder_frame_host ? embedder_frame_host->GetView()
@@ -212,13 +222,19 @@ void AutofillPopup::Hide() {
void AutofillPopup::SetItems(const std::vector<std::u16string>& values,
const std::vector<std::u16string>& labels) {
DCHECK(view_);
values_ = values;
labels_ = labels;
if (!view_)
return;
UpdatePopupBounds();
view_->OnSuggestionsChanged();
if (view_) // could be hidden after the change
view_->DoUpdateBoundsAndRedrawPopup();
if (!view_)
return;
view_->DoUpdateBoundsAndRedrawPopup();
}
void AutofillPopup::AcceptSuggestion(int index) {

View File

@@ -67,9 +67,18 @@ AutofillPopupView::~AutofillPopupView() {
}
void AutofillPopupView::Show() {
bool visible = parent_widget_->IsVisible();
visible = visible || view_proxy_;
if (!popup_ || !visible || parent_widget_->IsClosed())
if (!popup_)
return;
DCHECK(parent_widget_);
// The parent Widget can outlive its NativeWidget during teardown.
// Don't initialize the popup after the native parent view is gone.
if (parent_widget_->IsClosed() || !parent_widget_->GetNativeView())
return;
const bool visible = view_proxy_ || parent_widget_->IsVisible();
if (!visible)
return;
const bool initialize_widget = !GetWidget();
@@ -232,10 +241,14 @@ void AutofillPopupView::DoUpdateBoundsAndRedrawPopup() {
if (!popup_)
return;
views::Widget* const widget = GetWidget();
if (!widget)
return;
// Clamp popup_bounds_ to ensure it's never zero-width.
popup_->popup_bounds_.Union(
gfx::Rect(popup_->popup_bounds_.origin(), gfx::Size(1, 1)));
GetWidget()->SetBounds(popup_->popup_bounds_);
widget->SetBounds(popup_->popup_bounds_);
if (view_proxy_.get()) {
view_proxy_->SetBounds(popup_->popup_bounds_in_view());
}

View File

@@ -102,6 +102,9 @@ bool ElectronDesktopWindowTreeHostWin::WidgetSizeIsClientSize() const {
bool ElectronDesktopWindowTreeHostWin::GetClientAreaInsets(
gfx::Insets* insets,
int frame_thickness) const {
if (native_window_view_->IsFullscreen())
return false;
if (!native_window_view_->has_frame()) {
const int thickness = ::GetSystemMetrics(SM_CXSIZEFRAME) +
::GetSystemMetrics(SM_CXPADDEDBORDER);

View File

@@ -33,6 +33,13 @@ class ScopedHString {
// Returns string.
operator HSTRING() const { return str_; }
// Resets and returns the address for use as an out-parameter. The returned
// HSTRING will be released via WindowsDeleteString on destruction.
HSTRING* Receive() {
Reset();
return &str_;
}
// Whether there is a string created.
bool success() const { return str_; }

View File

@@ -253,8 +253,10 @@ bool Converter<net::HttpRequestHeaders>::FromV8(v8::Isolate* isolate,
if (!ConvertFromV8(isolate, val, &dict))
return false;
for (const auto it : dict) {
if (it.second.is_string())
if (it.second.is_string() && net::HttpUtil::IsValidHeaderName(it.first) &&
net::HttpUtil::IsValidHeaderValue(it.second.GetString())) {
out->SetHeader(it.first, std::move(it.second).TakeString());
}
}
return true;
}

View File

@@ -186,6 +186,8 @@ inline constexpr std::string_view kUseSharedTexture = "useSharedTexture";
inline constexpr std::string_view kSharedTexturePixelFormat =
"sharedTexturePixelFormat";
inline constexpr std::string_view kDeviceScaleFactor = "deviceScaleFactor";
inline constexpr std::string_view kNodeIntegrationInSubFrames =
"nodeIntegrationInSubFrames";

View File

@@ -252,6 +252,13 @@ void WebWorkerObserver::ContextWillDestroy(v8::Local<v8::Context> context) {
.ToLocal(&emit_v) &&
emit_v->IsFunction()) {
v8::Local<v8::Value> args[] = {gin::StringToV8(isolate, "exit")};
// Worker/worklet contexts use kScoped microtask policy (set by
// Blink). V8 DCHECKs that a MicrotasksScope exists around every
// Call() under that policy. We use kDoNotRunMicrotasks because
// the context is mid-teardown.
v8::MicrotasksScope microtasks_scope{
isolate, ctx->GetMicrotaskQueue(),
v8::MicrotasksScope::kDoNotRunMicrotasks};
v8::TryCatch try_catch(isolate);
emit_v.As<v8::Function>()
->Call(ctx, env->process_object(), 1, args)

View File

@@ -9,16 +9,32 @@ import * as fs from 'node:fs';
import * as http from 'node:http';
import * as https from 'node:https';
import * as net from 'node:net';
import * as os from 'node:os';
import * as path from 'node:path';
import * as readline from 'node:readline';
import { setTimeout } from 'node:timers/promises';
import { promisify } from 'node:util';
import { collectStreamBody, getResponse } from './lib/net-helpers';
import { ifdescribe, ifit, listen, waitUntil } from './lib/spec-helpers';
import { defer, ifdescribe, ifit, listen, waitUntil } from './lib/spec-helpers';
import { closeWindow, closeAllWindows } from './lib/window-helpers';
const fixturesPath = path.resolve(__dirname, 'fixtures');
const xdgMockFixturePath = path.join(fixturesPath, 'api', 'xdg-mock');
function makeXdgMockDirectories (prefix: string) {
const xdgDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
fs.cpSync(xdgMockFixturePath, xdgDir, { recursive: true });
const xdgDataHome = path.join(xdgDir, 'data');
const xdgConfigHome = path.join(xdgDir, 'config');
const xdgBinDir = path.join(xdgDir, 'bin');
fs.chmodSync(path.join(xdgBinDir, 'xdg-mime'), 0o755);
fs.chmodSync(path.join(xdgBinDir, 'xdg-settings'), 0o755);
return { xdgDir, xdgDataHome, xdgConfigHome, xdgBinDir };
}
const isMacOSx64 = process.platform === 'darwin' && process.arch === 'x64';
@@ -1435,6 +1451,94 @@ describe('app module', () => {
});
});
ifdescribe(process.platform === 'linux')('default protocol client APIs with mocked XDG settings', () => {
const protocol = 'electron-test-linux';
const desktopFileId = 'electron-test.desktop';
const protocolMimeType = `x-scheme-handler/${protocol}`;
let xdgDir: string;
let xdgDataHome: string;
let xdgConfigHome: string;
let xdgBinDir: string;
let oldEnv: Record<string, string | undefined>;
const getRegisteredHandler = () => {
for (const list of [
path.join(xdgConfigHome, 'mimeapps.list'),
path.join(xdgDataHome, 'applications', 'mimeapps.list'),
path.join(xdgDataHome, 'applications', 'defaults.list')
]) {
if (!fs.existsSync(list)) continue;
const match = fs
.readFileSync(list, 'utf8')
.split('\n')
.find((line) => line.startsWith(`${protocolMimeType}=`));
if (match) return match.split('=', 2)[1].split(';', 1)[0];
}
return '';
};
beforeEach(() => {
({ xdgDir, xdgDataHome, xdgConfigHome, xdgBinDir } = makeXdgMockDirectories('electron-xdg-default-client-'));
oldEnv = {
PATH: process.env.PATH,
CHROME_DESKTOP: process.env.CHROME_DESKTOP,
XDG_DATA_HOME: process.env.XDG_DATA_HOME,
XDG_DATA_DIRS: process.env.XDG_DATA_DIRS,
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME
};
defer(() => {
for (const [key, value] of Object.entries(oldEnv)) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
fs.rmSync(xdgDir, { recursive: true, force: true });
});
process.env.PATH = [xdgBinDir, oldEnv.PATH].filter(Boolean).join(':');
process.env.XDG_DATA_HOME = xdgDataHome;
process.env.XDG_DATA_DIRS = [xdgDataHome, oldEnv.XDG_DATA_DIRS].filter(Boolean).join(':');
process.env.XDG_CONFIG_HOME = xdgConfigHome;
app.setDesktopName(desktopFileId);
});
it('writes the default handler to the XDG association files', async () => {
expect(getRegisteredHandler()).to.equal('');
expect(app.setAsDefaultProtocolClient(protocol)).to.equal(true);
await waitUntil(() => getRegisteredHandler() === desktopFileId);
expect(getRegisteredHandler()).to.equal(desktopFileId);
});
it('detects whether the app is the default protocol client', async () => {
expect(app.isDefaultProtocolClient(protocol)).to.equal(false);
fs.writeFileSync(
path.join(xdgConfigHome, 'mimeapps.list'),
['[Default Applications]', `${protocolMimeType}=other.desktop`].join('\n')
);
expect(app.isDefaultProtocolClient(protocol)).to.equal(false);
fs.writeFileSync(
path.join(xdgConfigHome, 'mimeapps.list'),
['[Default Applications]', `${protocolMimeType}=${desktopFileId}`].join('\n')
);
await waitUntil(() => app.isDefaultProtocolClient(protocol));
expect(app.isDefaultProtocolClient(protocol)).to.equal(true);
});
});
describe('getApplicationNameForProtocol()', () => {
// TODO: Linux CI doesn't have registered http & https handlers
ifit(!(process.env.CI && process.platform === 'linux'))('returns application names for common protocols', function () {

View File

@@ -6094,6 +6094,22 @@ describe('BrowserWindow module', () => {
expect(w.isMenuBarVisible()).to.be.false('isMenuBarVisible');
});
for (const frame of [true, false]) {
it(`fills the display completely with content (frame: ${frame})`, () => {
const display = screen.getPrimaryDisplay();
const w = new BrowserWindow({
show: true,
frame,
// TODO(mitchchn): The menubar does not go away immediately
// on enter-full-screen/show so hide to avoid arbitary timeout.
autoHideMenuBar: true,
fullscreen: true
});
expectBoundsEqual(w.getBounds(), display.bounds);
expectBoundsEqual(w.getContentBounds(), display.bounds);
});
}
});
ifdescribe(process.platform === 'darwin')('fullscreenable state', () => {
@@ -6883,6 +6899,7 @@ describe('BrowserWindow module', () => {
expect(data.constructor.name).to.equal('NativeImage');
expect(data.isEmpty()).to.be.false('data is empty');
const size = data.getSize();
// TODO(reito): Use scale factor 1.0f when Electron 42.
const { scaleFactor } = screen.getPrimaryDisplay();
expect(size.width).to.be.closeTo(100 * scaleFactor, 2);
expect(size.height).to.be.closeTo(100 * scaleFactor, 2);
@@ -7025,6 +7042,66 @@ describe('BrowserWindow module', () => {
});
});
describe('offscreen rendering with device scale factor', () => {
let w: BrowserWindow;
const scaleFactor = 1.5;
beforeEach(function () {
w = new BrowserWindow({
width: 100,
height: 100,
show: false,
webPreferences: {
backgroundThrottling: false,
offscreen: {
deviceScaleFactor: scaleFactor
}
}
});
});
afterEach(closeAllWindows);
it('creates offscreen window with correct size considering device scale factor', async () => {
const paint = once(w.webContents, 'paint') as Promise<[any, Electron.Rectangle, Electron.NativeImage]>;
w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
const [, , data] = await paint;
expect(data.constructor.name).to.equal('NativeImage');
expect(data.isEmpty()).to.be.false('data is empty');
const size = data.getSize();
expect(size.width).to.be.closeTo(100 * scaleFactor, 2);
expect(size.height).to.be.closeTo(100 * scaleFactor, 2);
});
it('has correct screen and window sizes', async () => {
w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
await once(w.webContents, 'dom-ready');
const sizes = await w.webContents.executeJavaScript(`
new Promise((resolve) => {
const screenSize = [screen.width, screen.height];
const outerSize = [window.outerWidth, window.outerHeight];
const dpr = window.devicePixelRatio;
resolve({ screenSize, outerSize, dpr });
});
`);
expect(sizes.screenSize).to.deep.equal([100, 100]);
expect(sizes.outerSize).to.deep.equal([100, 100]);
expect(sizes.dpr).to.be.equal(scaleFactor);
});
it('has correct device screen size media query result', async () => {
w.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
await once(w.webContents, 'dom-ready');
const query = `(device-width: ${100}px)`;
const matches = await w.webContents.executeJavaScript(`
new Promise((resolve) => {
const mediaQuery = window.matchMedia('${query}');
resolve(mediaQuery.matches);
});
`);
expect(matches).to.be.true();
});
});
describe('"transparent" option', () => {
afterEach(closeAllWindows);

View File

@@ -1,12 +1,16 @@
import { app, contentTracing, TraceConfig, TraceCategoriesAndOptions } from 'electron/main';
import { app, contentTracing, EnableHeapProfilingOptions, TraceConfig, TraceCategoriesAndOptions } from 'electron/main';
import { expect } from 'chai';
import { once } from 'node:events';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import { ifdescribe } from './lib/spec-helpers';
import { ifdescribe, ifit, startRemoteControlApp } from './lib/spec-helpers';
const isCI = !!process.env.CI;
const fixturesPath = path.resolve(__dirname, 'fixtures');
// FIXME: The tests are skipped on linux arm/arm64
ifdescribe(!(['arm', 'arm64'].includes(process.arch)) || (process.platform !== 'linux'))('contentTracing', () => {
@@ -162,6 +166,252 @@ ifdescribe(!(['arm', 'arm64'].includes(process.arch)) || (process.platform !== '
});
});
describe('enableHeapProfiling', function () {
const enableHeapProfilingTestTimeout = 120000;
this.timeout(enableHeapProfilingTestTimeout);
const checkForHeapDumps = async (options?: EnableHeapProfilingOptions | false) => {
const rc = await startRemoteControlApp([`--remote-app-timeout=${enableHeapProfilingTestTimeout}`]);
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } = await rc.remotely(
async (
htmlPath: string,
utilityProcessPath: string,
options: EnableHeapProfilingOptions | false | undefined,
isCI: boolean
) => {
const { contentTracing, BrowserWindow, utilityProcess } = require('electron');
const { once } = require('node:events');
const fs = require('node:fs');
const process = require('node:process');
const { setTimeout } = require('node:timers/promises');
const isEventWithNonEmptyHeapDumpForProcess = (event: any, pid: number) =>
event.cat === 'disabled-by-default-memory-infra' &&
event.name === 'periodic_interval' &&
event.pid === pid &&
event.args.dumps.level_of_detail === 'detailed' &&
event.args.dumps.process_mmaps?.vm_regions.length > 0 &&
typeof event.args.dumps.allocators === 'object' &&
typeof event.args.dumps.heaps_v2.allocators === 'object' &&
Object.values(event.args.dumps.allocators).some((allocator: any) => allocator.attrs.size?.value !== '0') &&
Object.values(event.args.dumps.heaps_v2.allocators).some(
(allocator: any) =>
allocator.counts.length > 0 && allocator.nodes.length > 0 && allocator.sizes.length > 0
);
const hasNonEmptyHeapDumpForProcess = (parsedTrace: any, pid: number) =>
parsedTrace.traceEvents.some((event: any) => isEventWithNonEmptyHeapDumpForProcess(event, pid));
if (options !== false) await contentTracing.enableHeapProfiling(options);
await contentTracing.startRecording({
included_categories: ['disabled-by-default-memory-infra'],
excluded_categories: ['*'],
memory_dump_config: {
triggers: [{ mode: 'detailed', periodic_interval_ms: 1000 }]
}
});
// Launch a renderer process
const window = new BrowserWindow({ show: false });
await window.webContents.loadFile(htmlPath);
// Launch a utility process
const utility = utilityProcess.fork(utilityProcessPath);
await once(utility, 'spawn');
// Collect heap dumps
// - We wait for a long time because sometimes processes take a few seconds to start sending heap dumps.
// - CI machines are slower, so we wait longer there than when running locally.
await setTimeout(isCI ? 10000 : 4000);
const path = await contentTracing.stopRecording();
const data = fs.readFileSync(path, 'utf8');
const parsed = JSON.parse(data);
const hasBrowserProcessHeapDump = hasNonEmptyHeapDumpForProcess(parsed, process.pid);
const hasRendererProcessHeapDump = hasNonEmptyHeapDumpForProcess(parsed, window.webContents.getOSProcessId());
const hasUtilityProcessHeapDump = hasNonEmptyHeapDumpForProcess(parsed, utility.pid);
global.setTimeout(() => require('electron').app.quit());
return {
hasBrowserProcessHeapDump,
hasRendererProcessHeapDump,
hasUtilityProcessHeapDump
};
},
path.join(fixturesPath, 'api', 'content-tracing', 'index.html'),
path.join(fixturesPath, 'api', 'content-tracing', 'utility.js'),
options,
isCI
);
const [code] = await once(rc.process, 'exit');
expect(code).to.equal(0);
return {
hasBrowserProcessHeapDump,
hasRendererProcessHeapDump,
hasUtilityProcessHeapDump
};
};
it('does not include heap dumps when enableHeapProfiling is not called', async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps(false);
expect(hasBrowserProcessHeapDump).to.be.false();
expect(hasRendererProcessHeapDump).to.be.false();
expect(hasUtilityProcessHeapDump).to.be.false();
});
ifit(!process.env.IS_ASAN)(
'includes heap dumps for browser process when called with { mode: "browser" }',
async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps({ mode: 'browser' });
expect(hasBrowserProcessHeapDump).to.be.true();
expect(hasRendererProcessHeapDump).to.be.false();
expect(hasUtilityProcessHeapDump).to.be.false();
}
);
ifit(!process.env.IS_ASAN)(
'includes heap dumps for renderer processes when called with { mode: "all-renderers" }',
async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps({ mode: 'all-renderers' });
expect(hasBrowserProcessHeapDump).to.be.false();
expect(hasRendererProcessHeapDump).to.be.true();
expect(hasUtilityProcessHeapDump).to.be.false();
}
);
ifit(!process.env.IS_ASAN)(
'includes heap dumps for utility processes when called with { mode: "all-utilities" }',
async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps({ mode: 'all-utilities' });
expect(hasBrowserProcessHeapDump).to.be.false();
expect(hasRendererProcessHeapDump).to.be.false();
expect(hasUtilityProcessHeapDump).to.be.true();
}
);
ifit(!process.env.IS_ASAN)(
'includes heap dumps for browser, renderer, and utility processes when called with { mode: "all" }',
async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps({ mode: 'all' });
expect(hasBrowserProcessHeapDump).to.be.true();
expect(hasRendererProcessHeapDump).to.be.true();
expect(hasUtilityProcessHeapDump).to.be.true();
}
);
ifit(!process.env.IS_ASAN)(
'includes heap dumps for browser, renderer, and utility processes when called without options',
async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps();
expect(hasBrowserProcessHeapDump).to.be.true();
expect(hasRendererProcessHeapDump).to.be.true();
expect(hasUtilityProcessHeapDump).to.be.true();
}
);
ifit(!process.env.IS_ASAN)('accepts valid options', async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps({
mode: 'all',
stackMode: 'native-with-thread-names',
samplingRate: 50000
});
expect(hasBrowserProcessHeapDump).to.be.true();
expect(hasRendererProcessHeapDump).to.be.true();
expect(hasUtilityProcessHeapDump).to.be.true();
});
ifit(!process.env.IS_ASAN)('does not crash when invalid options are passed', async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps({
// @ts-expect-error Invalid mode
mode: 'invalid',
// @ts-expect-error Invalid stack mode
stackMode: 'invalid',
samplingRate: -1000
});
expect(hasBrowserProcessHeapDump).to.be.true();
expect(hasRendererProcessHeapDump).to.be.true();
expect(hasUtilityProcessHeapDump).to.be.true();
});
ifit(!process.env.IS_ASAN)('does not crash when options of invalid types are passed', async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps({
// @ts-expect-error Invalid mode
mode: { invalid: true },
// @ts-expect-error Invalid stack mode
stackMode: 999,
// @ts-expect-error Invalid sampling rate
samplingRate: 'invalid'
});
expect(hasBrowserProcessHeapDump).to.be.true();
expect(hasRendererProcessHeapDump).to.be.true();
expect(hasUtilityProcessHeapDump).to.be.true();
});
ifit(!!process.env.IS_ASAN)('does not include heap dumps in ASAN builds', async function () {
const { hasBrowserProcessHeapDump, hasRendererProcessHeapDump, hasUtilityProcessHeapDump } =
await checkForHeapDumps();
expect(hasBrowserProcessHeapDump).to.be.false();
expect(hasRendererProcessHeapDump).to.be.false();
expect(hasUtilityProcessHeapDump).to.be.false();
});
ifit(!process.env.IS_ASAN)('rejects when called multiple times', async function () {
const rc = await startRemoteControlApp();
const [firstResult, secondResult, thirdResult] = await rc.remotely(async () => {
const { contentTracing } = require('electron');
// Call twice before enabling finishes.
const firstPromise = contentTracing.enableHeapProfiling();
const secondPromise = contentTracing.enableHeapProfiling();
const [firstResult, secondResult] = await Promise.allSettled([firstPromise, secondPromise]);
// Call again after enabling finishes.
const thirdPromise = contentTracing.enableHeapProfiling();
const [thirdResult] = await Promise.allSettled([thirdPromise]);
global.setTimeout(() => require('electron').app.quit());
return [firstResult, secondResult, thirdResult];
});
const [code] = await once(rc.process, 'exit');
expect(code).to.equal(0);
expect(firstResult.status).to.equal('fulfilled');
expect(secondResult.status).to.equal('rejected');
expect(secondResult.reason.message).to.equal('Heap profiling is already enabled');
expect(thirdResult.status).to.equal('rejected');
expect(thirdResult.reason.message).to.equal('Heap profiling is already enabled');
});
});
describe('captured events', () => {
it('include V8 samples from the main process', async function () {
this.timeout(60000);

View File

@@ -180,6 +180,44 @@ describe('debugger module', () => {
await loadingFinished;
});
it('can continue a Fetch-paused document navigation without webRequest listeners', async () => {
server = http.createServer((_req, res) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end('<!DOCTYPE html><html><body><h1>Hello World</h1></body></html>');
});
const { port } = await listen(server);
const url = `http://localhost:${port}`;
const continueRequests: Array<Promise<any>> = [];
const onMessage = (_event: Electron.Event, method: string, params: any) => {
if (method === 'Fetch.requestPaused') {
continueRequests.push(
w.webContents.debugger.sendCommand('Fetch.continueRequest', {
requestId: params.requestId
})
);
}
};
w.webContents.debugger.attach();
w.webContents.debugger.on('message', onMessage);
try {
await w.webContents.debugger.sendCommand('Fetch.enable', {
patterns: [{ resourceType: 'Document' }]
});
await expect(w.loadURL(url)).to.eventually.be.fulfilled();
await Promise.all(continueRequests);
} finally {
w.webContents.debugger.off('message', onMessage);
if (w.webContents.debugger.isAttached()) {
w.webContents.debugger.detach();
}
}
});
it('can get and set cookies using the Storage API', async () => {
await w.webContents.loadURL('about:blank');
w.webContents.debugger.attach('1.1');

View File

@@ -34,7 +34,7 @@ const unregisterProtocol = protocol.unregisterProtocol;
const uninterceptProtocol = protocol.uninterceptProtocol;
const text = 'valar morghulis';
const protocolName = 'no-cors';
const protocolName = 'cors';
const postData = {
name: 'post test',
type: 'string'
@@ -924,7 +924,149 @@ describe('protocol module', () => {
});
});
// DISABLED-FIXME: Figure out why this test is failing
// A scheme registered with only {supportFetchAPI: true} (no
// {corsEnabled: true}) must not be readable cross-origin.
describe('cross-origin enforcement for supportFetchAPI-only schemes', () => {
const secret = 'secret-token-9d4f2c';
let remoteUrl: string;
let handlerCalls: string[];
beforeEach(async () => {
handlerCalls = [];
protocol.handle('no-cors', (req) => {
handlerCalls.push(req.url);
return new Response(secret, { headers: { 'content-type': 'text/plain' } });
});
protocol.handle('no-cors-standard', (req) => {
handlerCalls.push(req.url);
if (new URL(req.url).pathname === '/page') {
return new Response('<!doctype html><body>page', {
headers: { 'content-type': 'text/html' }
});
}
return new Response(secret, { headers: { 'content-type': 'text/plain' } });
});
const server = http.createServer((req, res) => {
res.setHeader('content-type', 'text/html');
res.end('<!doctype html><body>remote');
});
defer(() => server.close());
({ url: remoteUrl } = await listen(server));
});
afterEach(() => {
protocol.unhandle('no-cors');
protocol.unhandle('no-cors-standard');
});
it('blocks a remote http origin from reading the response body via fetch()', async () => {
await w.loadURL(remoteUrl);
const consoleMessages: string[] = [];
w.webContents.on('console-message', (e) => consoleMessages.push(e.message));
const { body, error } = await w.webContents.executeJavaScript(`
fetch('no-cors://host/secret')
.then(r => r.text()).then(body => ({ body, error: null }))
.catch(e => ({ body: null, error: String(e) }))
`);
expect(body).to.not.equal(secret, 'http origin read no-cors:// body via fetch()');
expect(error)
.to.be.a('string')
.and.match(/Failed to fetch/);
expect(consoleMessages.join('\n')).to.match(/has been blocked by CORS policy/);
});
it('blocks a remote http origin from reading the response body via XHR', async () => {
await w.loadURL(remoteUrl);
const { body, errored } = await w.webContents.executeJavaScript(`
new Promise(resolve => {
const x = new XMLHttpRequest();
x.onload = () => resolve({ body: x.responseText, errored: false });
x.onerror = () => resolve({ body: null, errored: true });
x.open('GET', 'no-cors://host/secret');
x.send();
})
`);
expect(body).to.not.equal(secret, 'http origin read no-cors:// body via XHR');
expect(errored).to.be.true();
});
it('does not invoke the protocol handler for a blocked cross-origin CORS request', async () => {
await w.loadURL(remoteUrl);
await w.webContents.executeJavaScript(`
fetch('no-cors://host/secret', {
method: 'PUT',
headers: { 'x-custom': '1' },
body: 'x'
}).catch(() => 0)
`);
expect(handlerCalls).to.deep.equal([]);
});
it('returns an opaque response for cross-origin fetch with mode: no-cors', async () => {
await w.loadURL(remoteUrl);
const { type, status, body } = await w.webContents.executeJavaScript(`
fetch('no-cors://host/secret', { mode: 'no-cors' })
.then(async r => ({ type: r.type, status: r.status, body: await r.text() }))
`);
expect(type).to.equal('opaque');
expect(status).to.equal(0);
expect(body).to.equal('');
});
it('still allows cross-origin <img> loads (no-cors subresource)', async () => {
protocol.unhandle('no-cors');
protocol.handle(
'no-cors',
() =>
new Response(fs.readFileSync(path.join(fixturesPath, 'assets', 'logo.png')), {
headers: { 'content-type': 'image/png' }
})
);
await w.loadURL(remoteUrl);
const { ok, width } = await w.webContents.executeJavaScript(`
new Promise(resolve => {
const img = new Image();
img.onload = () => resolve({ ok: true, width: img.naturalWidth });
img.onerror = () => resolve({ ok: false, width: 0 });
img.src = 'no-cors://host/logo.png';
})
`);
expect(ok).to.be.true();
expect(width).to.be.greaterThan(0);
});
it('allows same-origin fetch on a standard supportFetchAPI-only scheme', async () => {
await w.loadURL('no-cors-standard://app/page');
const body = await w.webContents.executeJavaScript("fetch('no-cors-standard://app/data').then(r => r.text())");
expect(body).to.equal(secret);
});
it('blocks cross-origin fetch on a standard supportFetchAPI-only scheme', async () => {
await w.loadURL('no-cors-standard://app/page');
handlerCalls = [];
const error = await w.webContents.executeJavaScript(
"fetch('no-cors-standard://other/data').then(() => null, e => String(e))"
);
expect(error)
.to.be.a('string')
.and.match(/Failed to fetch/);
expect(handlerCalls).to.deep.equal([]);
});
it('does not affect cross-origin fetch to a corsEnabled scheme', async () => {
protocol.handle('cors', () => new Response('ok'));
defer(() => protocol.unhandle('cors'));
await w.loadURL(remoteUrl);
const body = await w.webContents.executeJavaScript("fetch('cors://host/').then(r => r.text())");
expect(body).to.equal('ok');
});
it('does not affect main-process net.fetch', async () => {
const body = await net.fetch('no-cors://host/secret').then((r) => r.text());
expect(body).to.equal(secret);
});
});
it('disallows CORS and fetch requests when only supportFetchAPI is specified', async () => {
await allowsCORSRequests('no-cors', ['failed xhr', 'failed fetch'], /has been blocked by CORS policy/, () => {
const { ipcRenderer } = require('electron');
@@ -1489,9 +1631,9 @@ describe('protocol module', () => {
});
it('can receive streaming fetch upload', async () => {
protocol.handle('no-cors', (req) => new Response(req.body));
defer(() => { protocol.unhandle('no-cors'); });
await contents.loadURL('no-cors://foo/');
protocol.handle('cors', (req) => new Response(req.body));
defer(() => { protocol.unhandle('cors'); });
await contents.loadURL('cors://foo/');
const fetchBodyResult = await contents.executeJavaScript(`
const stream = new ReadableStream({
async start(controller) {
@@ -1513,12 +1655,12 @@ describe('protocol module', () => {
session.defaultSession.webRequest.onBeforeRequest(null);
});
protocol.handle('no-cors', (req) => {
protocol.handle('cors', (req) => {
console.log('handle', req.url, req.method);
return new Response(req.body);
});
defer(() => { protocol.unhandle('no-cors'); });
await contents.loadURL('no-cors://foo/');
defer(() => { protocol.unhandle('cors'); });
await contents.loadURL('cors://foo/');
const fetchBodyResult = await contents.executeJavaScript(`
const stream = new ReadableStream({
async start(controller) {
@@ -1532,9 +1674,9 @@ describe('protocol module', () => {
});
it('can receive an error from streaming fetch upload', async () => {
protocol.handle('no-cors', (req) => new Response(req.body));
defer(() => { protocol.unhandle('no-cors'); });
await contents.loadURL('no-cors://foo/');
protocol.handle('cors', (req) => new Response(req.body));
defer(() => { protocol.unhandle('cors'); });
await contents.loadURL('cors://foo/');
const fetchBodyResult = await contents.executeJavaScript(`
const stream = new ReadableStream({
async start(controller) {
@@ -1549,12 +1691,12 @@ describe('protocol module', () => {
it('gets an error from streaming fetch upload when the renderer dies', async () => {
let gotRequest: Function;
const receivedRequest = new Promise<Request>(resolve => { gotRequest = resolve; });
protocol.handle('no-cors', (req) => {
protocol.handle('cors', (req) => {
if (/fetch/.test(req.url)) gotRequest(req);
return new Response();
});
defer(() => { protocol.unhandle('no-cors'); });
await contents.loadURL('no-cors://foo/');
defer(() => { protocol.unhandle('cors'); });
await contents.loadURL('cors://foo/');
contents.executeJavaScript(`
const stream = new ReadableStream({
async start(controller) {

View File

@@ -551,9 +551,10 @@ describe('webContents module', () => {
w.loadURL('data:text/html,<h1>HELLO</h1>');
});
it('fails if loadurl is called after the navigation is ready to commit', () => {
it('fails if loadurl is called after the navigation is ready to commit', (done) => {
w.webContents.once('did-fail-load', (_event, _errorCode, _errorDescription, validatedURL) => {
expect(validatedURL).to.contain('blank.html');
done();
});
// @ts-expect-error internal-only event.

View File

@@ -356,12 +356,12 @@ describe('webRequest module', () => {
});
it('can change the request headers on a custom protocol redirect', async () => {
protocol.registerStringProtocol('no-cors', (req, callback) => {
if (req.url === 'no-cors://fake-host/redirect') {
protocol.registerStringProtocol('cors-blob', (req, callback) => {
if (req.url === 'cors-blob://fake-host/redirect') {
callback({
statusCode: 302,
headers: {
Location: 'no-cors://fake-host'
Location: 'cors-blob://fake-host'
}
});
} else {
@@ -384,10 +384,10 @@ describe('webRequest module', () => {
requestHeaders.Accept = '*/*;test/header';
callback({ requestHeaders });
});
const { data } = await ajax('no-cors://fake-host/redirect');
const { data } = await ajax('cors-blob://fake-host/redirect');
expect(data).to.equal('header-received');
} finally {
protocol.unregisterProtocol('no-cors');
protocol.unregisterProtocol('cors-blob');
}
});
@@ -411,6 +411,27 @@ describe('webRequest module', () => {
expect(called).to.be.true();
});
it('does not crash on invalid header name or value', async () => {
ses.webRequest.onBeforeSendHeaders((details, callback) => {
const requestHeaders = details.requestHeaders;
requestHeaders['Invalid Header'] = 'valid-value';
requestHeaders['Valid-Header'] = 'invalid\r\nvalue';
requestHeaders['X-Good'] = 'good-value';
callback({ requestHeaders });
});
const sentHeaders = new Promise<Electron.OnSendHeadersListenerDetails>((resolve) => {
ses.webRequest.onSendHeaders(resolve);
});
const { data } = await ajax(defaultURL);
const details = await sentHeaders;
expect(details.requestHeaders['Invalid Header']).to.be.undefined();
expect(details.requestHeaders['Valid-Header']).to.be.undefined();
expect(details.requestHeaders['X-Good']).to.equal('good-value');
expect(data).to.equal('/');
});
it('resets the whole headers', async () => {
const requestHeaders = {
Test: 'header'

View File

@@ -966,6 +966,17 @@ describe('chromium features', () => {
});
});
describe('<geolocation> element', () => {
afterEach(closeAllWindows);
it('does not crash the renderer', (done) => {
const w = new BrowserWindow({ show: false });
w.webContents.once('did-finish-load', () => done());
w.webContents.once('render-process-gone', () => done(new Error('renderer crashed / was killed')));
w.loadURL('data:text/html,<geolocation></geolocation>');
});
});
describe('File System API,', () => {
let w: BrowserWindow | null = null;

View File

@@ -3,7 +3,6 @@
"BrowserWindow module BrowserWindow.loadURL(url) should emit did-fail-load event for files that do not exist",
"Menu module Menu.setApplicationMenu unsets a menu with null",
"process module main process process.takeHeapSnapshot() returns true on success",
"protocol module protocol.registerSchemesAsPrivileged cors-fetch disallows CORS and fetch requests when only supportFetchAPI is specified",
"session module ses.cookies should set cookie for standard scheme",
"webFrameMain module WebFrame.visibilityState should match window state",
"reporting api sends a report for a deprecation",

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>

View File

@@ -0,0 +1,3 @@
setInterval(() => {
new Array(1000000).fill(0);
}, 100);

15
spec/fixtures/api/xdg-mock/bin/xdg-mime vendored Normal file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
if [ "$1" != "query" ] || [ "$2" != "default" ]; then
exit 1
fi
mime="$3"
for list in "$XDG_CONFIG_HOME/mimeapps.list" "$XDG_DATA_HOME/applications/mimeapps.list" "$XDG_DATA_HOME/applications/defaults.list"; do
if [ -f "$list" ]; then
result=$(grep "^$mime=" "$list" | head -n 1 | cut -d= -f2 | cut -d";" -f1)
if [ -n "$result" ]; then
printf "%s\n" "$result"
exit 0
fi
fi
done
exit 0

View File

@@ -0,0 +1,31 @@
#!/bin/sh
set -eu
get_handler() {
for list in "$XDG_CONFIG_HOME/mimeapps.list" "$XDG_DATA_HOME/applications/mimeapps.list" "$XDG_DATA_HOME/applications/defaults.list"; do
if [ -f "$list" ]; then
result=$(grep "^x-scheme-handler/$1=" "$list" | head -n 1 | cut -d= -f2 | cut -d";" -f1)
if [ -n "$result" ]; then
printf "%s\n" "$result"
return 0
fi
fi
done
return 1
}
if [ "$1" = "set" ] && [ "$2" = "default-url-scheme-handler" ]; then
mkdir -p "$XDG_CONFIG_HOME"
{
printf "[Default Applications]\n"
printf "x-scheme-handler/%s=%s\n" "$3" "$4"
} > "$XDG_CONFIG_HOME/mimeapps.list"
exit 0
fi
if [ "$1" = "check" ] && [ "$2" = "default-url-scheme-handler" ]; then
if [ "$(get_handler "$3" 2>/dev/null || true)" = "$4" ]; then
printf "yes\n"
else
printf "no\n"
fi
exit 0
fi
exit 1

View File

@@ -0,0 +1,5 @@
[Default Applications]
x-scheme-handler/mockproto=mock-browser.desktop
[Added Associations]
x-scheme-handler/mockproto=mock-browser.desktop;

View File

@@ -0,0 +1,5 @@
[Desktop Entry]
Name=Electron Test
Exec=/usr/bin/true %u
Type=Application
MimeType=x-scheme-handler/electron-test-linux;

View File

@@ -0,0 +1,5 @@
[Desktop Entry]
Name=Mock Browser
Exec=/usr/bin/true %u
Type=Application
MimeType=x-scheme-handler/mockproto;

View File

@@ -8,6 +8,21 @@ const http = require('node:http');
const promises_1 = require('node:timers/promises');
const v8 = require('node:v8');
function getAutoQuitTimeout () {
const argPrefix = '--remote-app-timeout=';
const arg = process.argv.find((arg) => arg.startsWith(argPrefix));
if (arg) {
const timeout = parseInt(arg.slice(argPrefix.length), 10);
if (Number.isSafeInteger(timeout) && timeout > 0) {
return timeout;
}
}
return 30000;
}
if (app.commandLine.hasSwitch('boot-eval')) {
// eslint-disable-next-line no-eval
eval(app.commandLine.getSwitchValue('boot-eval'));
@@ -35,4 +50,4 @@ app.whenReady().then(() => {
setTimeout(() => {
process.exit(0);
}, 30000);
}, getAutoQuitTimeout());

View File

@@ -0,0 +1,28 @@
const { app, contentTracing } = require('electron');
const assert = require('node:assert/strict');
(async () => {
// Before app is ready, all contentTracing methods should reject
// instead of crashing.
if (!app.isReady()) {
await assert.rejects(() => contentTracing.startRecording({ included_categories: ['*'] }), /before app is ready/);
await assert.rejects(() => contentTracing.stopRecording(), /before app is ready/);
await assert.rejects(() => contentTracing.getCategories(), /before app is ready/);
await assert.rejects(() => contentTracing.getTraceBufferUsage(), /before app is ready/);
}
await app.whenReady();
// After app is ready, startRecording should work normally.
await contentTracing.startRecording({ included_categories: ['*'] });
await contentTracing.stopRecording();
})()
.then(app.quit)
.catch((err) => {
console.error(err);
app.exit(1);
});

View File

@@ -55,6 +55,7 @@ protocol.registerSchemesAsPrivileged([
{ scheme: 'cors-blob', privileges: { corsEnabled: true, supportFetchAPI: true } },
{ scheme: 'cors', privileges: { corsEnabled: true, supportFetchAPI: true } },
{ scheme: 'no-cors', privileges: { supportFetchAPI: true } },
{ scheme: 'no-cors-standard', privileges: { standard: true, supportFetchAPI: true } },
{ scheme: 'no-fetch', privileges: { corsEnabled: true } },
{ scheme: 'stream', privileges: { standard: true, stream: true } },
{ scheme: 'foo', privileges: { standard: true } },