mirror of
https://github.com/electron/electron.git
synced 2026-05-02 03:00:22 -04:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff343d4808 | ||
|
|
5562011d1d | ||
|
|
c9a1cff273 | ||
|
|
8d1475e70b | ||
|
|
7e0499d55d | ||
|
|
86b483af5b | ||
|
|
3cd0af44cf | ||
|
|
f596e35554 | ||
|
|
9867bbbde8 | ||
|
|
6fd63cdba8 | ||
|
|
a629331619 | ||
|
|
248ccef775 | ||
|
|
dd74ff3451 | ||
|
|
d0afb91da4 | ||
|
|
b88bbb9ca6 | ||
|
|
dea65bddd4 | ||
|
|
57fee9f2c1 | ||
|
|
d6bc5b6753 | ||
|
|
51f35cf926 | ||
|
|
93cc936a94 | ||
|
|
48401169d9 | ||
|
|
c4965cb580 | ||
|
|
f24e43dc75 | ||
|
|
a3275d605c | ||
|
|
2674aa64b9 | ||
|
|
47d85799a5 | ||
|
|
ce0739ad4f | ||
|
|
285efaf87b |
24
.github/actions/build-image-sha/action.yml
vendored
Normal file
24
.github/actions/build-image-sha/action.yml
vendored
Normal 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
|
||||
21
.github/actions/install-dependencies/action.yml
vendored
21
.github/actions/install-dependencies/action.yml
vendored
@@ -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
|
||||
|
||||
132
.github/siso-patches/0002-siso-retry-transient-ERROR_INVALID_PARAMETER-when-op.patch
vendored
Normal file
132
.github/siso-patches/0002-siso-retry-transient-ERROR_INVALID_PARAMETER-when-op.patch
vendored
Normal 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
|
||||
|
||||
8
.github/workflows/apply-patches.yml
vendored
8
.github/workflows/apply-patches.yml
vendored
@@ -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
|
||||
|
||||
23
.github/workflows/build.yml
vendored
23
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
33
.github/workflows/linux-publish.yml
vendored
33
.github/workflows/linux-publish.yml
vendored
@@ -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
|
||||
|
||||
22
.github/workflows/macos-publish.yml
vendored
22
.github/workflows/macos-publish.yml
vendored
@@ -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
|
||||
|
||||
26
.github/workflows/windows-publish.yml
vendored
26
.github/workflows/windows-publish.yml
vendored
@@ -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
|
||||
|
||||
2
BUILD.gn
2
BUILD.gn
@@ -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
2
DEPS
@@ -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':
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
26
docs/api/structures/enable-heap-profiling-options.md
Normal file
26
docs/api/structures/enable-heap-profiling-options.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
BIN
docs/images/viewing-heap-dumps.png
Normal file
BIN
docs/images/viewing-heap-dumps.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 --"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
43
patches/chromium/cherry-pick-7687618.patch
Normal file
43
patches/chromium/cherry-pick-7687618.patch
Normal 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,
|
||||
@@ -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 = {
|
||||
@@ -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.
|
||||
@@ -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
|
||||
55
patches/chromium/cherry-pick-c75f63de7188.patch
Normal file
55
patches/chromium/cherry-pick-c75f63de7188.patch
Normal 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
|
||||
49
patches/chromium/cherry-pick-cve-2026-6920.patch
Normal file
49
patches/chromium/cherry-pick-cve-2026-6920.patch
Normal 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()) {
|
||||
211
patches/chromium/cherry-pick-d141d62357df.patch
Normal file
211
patches/chromium/cherry-pick-d141d62357df.patch
Normal 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 =
|
||||
@@ -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)
|
||||
|
||||
197
patches/chromium/cherry-pick-fccaeb9e0967.patch
Normal file
197
patches/chromium/cherry-pick-fccaeb9e0967.patch
Normal 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
|
||||
@@ -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
|
||||
@@ -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());
|
||||
|
||||
@@ -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",
|
||||
|
||||
22
patches/chromium/patch_osr_control_screen_info.patch
Normal file
22
patches/chromium/patch_osr_control_screen_info.patch
Normal 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
|
||||
@@ -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" }
|
||||
]
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
cherry-pick-4369bd1258dc.patch
|
||||
cherry-pick-a047955845e5.patch
|
||||
cherry-pick-c61e9586156f.patch
|
||||
cherry-pick-395efd18d8ef.patch
|
||||
|
||||
@@ -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]);
|
||||
@@ -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.
|
||||
@@ -1,2 +0,0 @@
|
||||
cherry-pick-ca8a943c247c.patch
|
||||
cherry-pick-bce2e6728279.patch
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
@@ -1,2 +1,2 @@
|
||||
cherry-pick-0566b2f5f0d1.patch
|
||||
cherry-pick-3f9969421ad5.patch
|
||||
cherry-pick-8c705ac86366.patch
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
101
patches/skia/cherry-pick-8c705ac86366.patch
Normal file
101
patches/skia/cherry-pick-8c705ac86366.patch
Normal 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();
|
||||
@@ -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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() &&
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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_;
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
10
spec/fixtures/api/content-tracing/index.html
vendored
Normal file
10
spec/fixtures/api/content-tracing/index.html
vendored
Normal 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>
|
||||
3
spec/fixtures/api/content-tracing/utility.js
vendored
Normal file
3
spec/fixtures/api/content-tracing/utility.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
setInterval(() => {
|
||||
new Array(1000000).fill(0);
|
||||
}, 100);
|
||||
15
spec/fixtures/api/xdg-mock/bin/xdg-mime
vendored
Normal file
15
spec/fixtures/api/xdg-mock/bin/xdg-mime
vendored
Normal 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
|
||||
31
spec/fixtures/api/xdg-mock/bin/xdg-settings
vendored
Normal file
31
spec/fixtures/api/xdg-mock/bin/xdg-settings
vendored
Normal 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
|
||||
5
spec/fixtures/api/xdg-mock/config/mimeapps.list
vendored
Normal file
5
spec/fixtures/api/xdg-mock/config/mimeapps.list
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
[Default Applications]
|
||||
x-scheme-handler/mockproto=mock-browser.desktop
|
||||
|
||||
[Added Associations]
|
||||
x-scheme-handler/mockproto=mock-browser.desktop;
|
||||
5
spec/fixtures/api/xdg-mock/data/applications/electron-test.desktop
vendored
Normal file
5
spec/fixtures/api/xdg-mock/data/applications/electron-test.desktop
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
[Desktop Entry]
|
||||
Name=Electron Test
|
||||
Exec=/usr/bin/true %u
|
||||
Type=Application
|
||||
MimeType=x-scheme-handler/electron-test-linux;
|
||||
5
spec/fixtures/api/xdg-mock/data/applications/mock-browser.desktop
vendored
Normal file
5
spec/fixtures/api/xdg-mock/data/applications/mock-browser.desktop
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
[Desktop Entry]
|
||||
Name=Mock Browser
|
||||
Exec=/usr/bin/true %u
|
||||
Type=Application
|
||||
MimeType=x-scheme-handler/mockproto;
|
||||
17
spec/fixtures/apps/remote-control/main.js
vendored
17
spec/fixtures/apps/remote-control/main.js
vendored
@@ -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());
|
||||
|
||||
28
spec/fixtures/crash-cases/content-tracing-before-ready/index.js
vendored
Normal file
28
spec/fixtures/crash-cases/content-tracing-before-ready/index.js
vendored
Normal 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);
|
||||
});
|
||||
@@ -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 } },
|
||||
|
||||
Reference in New Issue
Block a user