Skip to content

Commit d284168

Browse files
fix: crash calling OSR shared texture release() after texture GC'd (#50500)
The weak persistent tracking the OffscreenReleaseHolderMonitor was tied to the texture object, but the release() closure holds a raw pointer to the monitor via its v8::External data. If JS retained texture.release while dropping the texture itself, the monitor would be freed on GC and a later release() call would crash. Track the release function instead of the texture object. Since the texture holds release as a property, this keeps the monitor alive as long as either is reachable. Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Sam Attard <sattard@anthropic.com>
1 parent 4aa3610 commit d284168

File tree

2 files changed

+54
-3
lines changed

2 files changed

+54
-3
lines changed

shell/common/gin_converters/osr_converter.cc

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,12 @@ v8::Local<v8::Value> Converter<electron::OffscreenSharedTextureValue>::ToV8(
150150
root.Set("textureInfo", ConvertToV8(isolate, dict));
151151
auto root_local = ConvertToV8(isolate, root);
152152

153-
// Create a persistent reference of the object, so that we can check the
154-
// monitor again when GC collects this object.
155-
auto* tex_persistent = monitor->CreatePersistent(isolate, root_local);
153+
// Create a weak persistent that tracks the release function rather than the
154+
// texture object. The release function holds a raw pointer to |monitor| via
155+
// its v8::External data, so |monitor| must outlive it. Since the texture
156+
// keeps |release| alive via its property, this also covers the case where
157+
// the texture itself is leaked without calling release().
158+
auto* tex_persistent = monitor->CreatePersistent(isolate, releaser);
156159
tex_persistent->SetWeak(
157160
monitor,
158161
[](const v8::WeakCallbackInfo<OffscreenReleaseHolderMonitor>& data) {

spec/api-browser-window-spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6875,6 +6875,54 @@ describe('BrowserWindow module', () => {
68756875
expect(w.webContents.frameRate).to.equal(30);
68766876
});
68776877
});
6878+
6879+
describe('shared texture', () => {
6880+
const v8Util = process._linkedBinding('electron_common_v8_util');
6881+
6882+
it('does not crash when release() is called after the texture is garbage collected', async () => {
6883+
const sw = new BrowserWindow({
6884+
width: 100,
6885+
height: 100,
6886+
show: false,
6887+
webPreferences: {
6888+
backgroundThrottling: false,
6889+
offscreen: {
6890+
useSharedTexture: true
6891+
}
6892+
}
6893+
});
6894+
6895+
const paint = once(sw.webContents, 'paint') as Promise<[any, Electron.Rectangle, Electron.NativeImage]>;
6896+
sw.loadFile(path.join(fixtures, 'api', 'offscreen-rendering.html'));
6897+
const [event] = await paint;
6898+
sw.webContents.stopPainting();
6899+
6900+
if (!event.texture) {
6901+
// GPU shared texture not available on this host; skip.
6902+
sw.destroy();
6903+
return;
6904+
}
6905+
6906+
// Keep only the release closure and drop the owning texture object.
6907+
const staleRelease = event.texture.release;
6908+
const weakTexture = new WeakRef(event.texture);
6909+
event.texture = undefined;
6910+
6911+
// Force GC until the texture object is collected.
6912+
let collected = false;
6913+
for (let i = 0; i < 30 && !collected; ++i) {
6914+
await setTimeout();
6915+
v8Util.requestGarbageCollectionForTesting();
6916+
collected = weakTexture.deref() === undefined;
6917+
}
6918+
expect(collected).to.be.true('texture should be garbage collected');
6919+
6920+
// This should return safely and not crash the main process.
6921+
expect(() => staleRelease()).to.not.throw();
6922+
6923+
sw.destroy();
6924+
});
6925+
});
68786926
});
68796927

68806928
describe('offscreen rendering with device scale factor', () => {

0 commit comments

Comments
 (0)