Skip to content

Commit 1d0ee4e

Browse files
Hardening v2.7claude
andcommitted
Revert "fix(stealth): ..." — speculative follow-ups to #509
Reverts 86b5ad9 and 73dda11, restoring electron/WindowHelper.ts and the overlay tests to exactly the state PR #509 merged in. Neither commit was backed by a defect I could actually demonstrate: - The win32 skip on the chrome re-push rested on "a value-identical setContentProtection call causes DWM affinity churn", which is a comment in this repo, not something verified on Windows from here. It also narrowed a protective write on a stealth path — the wrong direction to move on an unverified premise. - The opacity-shield flush fixed a real control-flow gap (minimizeWindow / closeWindow discard a pending setOpacity(1)) but I never showed the race is reachable: it needs a minimize or close-to-tray IPC landing inside the 60ms shield window, while the launcher is transparent or hidden. The nulling of this.opacityTimeout only mattered as support for the flush, so it goes with it. The one finding that WAS demonstrated — the vacuous positive assertions in OverlayAlwaysContentProtected.test.mjs, proven by a mutation probe that reintroduced the leak at both creation sites and still gave 8/8 green — is in 63a4fe9 and stays, since it came in with the PR. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Si93L7isjy1GFkqzjb7gJy
1 parent 73dda11 commit 1d0ee4e

3 files changed

Lines changed: 10 additions & 285 deletions

File tree

electron/WindowHelper.ts

Lines changed: 8 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -279,33 +279,12 @@ export class WindowHelper {
279279
// default), re-exposing it via `NSWindowSharingReadOnly` and overriding the
280280
// unconditional `NSWindowSharingNone` the native stealth module applies to
281281
// exactly these three windows. Force it on for them regardless of `enable`.
282-
//
283-
// Skipped on Windows, deliberately. The chrome is already `true` from
284-
// creation (createWindow / createOverlayAuxWindows) and on every show
285-
// (switchToOverlay, both branches), and NOTHING in this codebase ever
286-
// passes `false` for these three — so on win32 this write is always
287-
// value-identical, and identical writes are exactly what the dedupe guard
288-
// in setContentProtection() exists to avoid: repeated DWM affinity churn
289-
// leaves the HWND in a transient black/blank frame state for a few hundred
290-
// ms (see SetContentProtectionDedupe.test.mjs). Before the always-protect
291-
// fix this write was value-CHANGING on every undetectable toggle, so it
292-
// paid for itself; now it would only blank the overlay mid-meeting when
293-
// the user flips undetectable mode.
294-
//
295-
// It is still required off win32: reassertContentProtection() routes here
296-
// after app.dock.hide()/show(), and that macOS activation-policy flip makes
297-
// WindowServer re-evaluate each NSWindow and silently reset its
298-
// sharingType. There the re-push is the whole point — the in-memory value
299-
// is already correct, so only an unconditional write restores the OS flag.
300-
// Windows has no equivalent policy flip.
301-
if (process.platform !== 'win32') {
302-
const overlayChrome = [this.overlayWindow, this.pillWindow, this.toggleWindow];
303-
overlayChrome.forEach((win) => {
304-
if (win && !win.isDestroyed()) {
305-
win.setContentProtection(true);
306-
}
307-
});
308-
}
282+
const overlayChrome = [this.overlayWindow, this.pillWindow, this.toggleWindow];
283+
overlayChrome.forEach((win) => {
284+
if (win && !win.isDestroyed()) {
285+
win.setContentProtection(true);
286+
}
287+
});
309288
// The launcher and popover catcher are not meeting chrome; they follow the
310289
// undetectable-mode toggle (the launcher is the main window shown outside a
311290
// meeting, and the native module deliberately does NOT force-hide it).
@@ -2309,11 +2288,6 @@ export class WindowHelper {
23092288

23102289
if (this.opacityTimeout) clearTimeout(this.opacityTimeout);
23112290
this.opacityTimeout = setTimeout(() => {
2312-
// Mark the shield spent. Without this the field holds an
2313-
// already-fired Timeout forever, so cancelOpacityShield()'s
2314-
// "nothing pending" early return never fires and every later
2315-
// minimize/close runs a pointless restore pass.
2316-
this.opacityTimeout = null;
23172291
if (this.overlayWindow && !this.overlayWindow.isDestroyed()) {
23182292
this.overlayWindow.setOpacity(1);
23192293
this.pillWindow?.setOpacity(1);
@@ -2412,8 +2386,6 @@ export class WindowHelper {
24122386

24132387
if (this.opacityTimeout) clearTimeout(this.opacityTimeout);
24142388
this.opacityTimeout = setTimeout(() => {
2415-
// Mark the shield spent — see the overlay branch above.
2416-
this.opacityTimeout = null;
24172389
if (this.launcherWindow && !this.launcherWindow.isDestroyed()) {
24182390
this.launcherWindow.setOpacity(1);
24192391
if (!inactive) this.launcherWindow.focus();
@@ -2537,48 +2509,10 @@ export class WindowHelper {
25372509
menu.popup({ window: win, x: point.x, y: point.y });
25382510
}
25392511

2540-
/**
2541-
* Cancel a pending opacity-shield timer WITHOUT stranding its windows at
2542-
* opacity 0.
2543-
*
2544-
* The Windows opacity shield (switchToOverlay / switchToLauncher) shows a
2545-
* window at opacity 0 and restores it 60ms later, once DWM has applied the
2546-
* capture-exclusion flag, so the first frame can't leak. A bare
2547-
* `clearTimeout` drops that restore on the floor: a minimize or close landing
2548-
* inside those 60ms left the shielded windows shown-but-fully-transparent
2549-
* until the next switch re-set their opacity. Flush the restore instead of
2550-
* discarding it — setting opacity 1 on a window that is about to be minimized
2551-
* or closed is harmless; leaving one stuck invisible is not.
2552-
*
2553-
* `isVisible()` is the guard that keeps this from fighting hideMainWindow,
2554-
* which deliberately zeroes opacity on win32 before hide() so a capture frame
2555-
* during the hide can't leak the chrome. Those windows are hidden by then, so
2556-
* they are skipped here and their opacity is re-set by the next show.
2557-
*
2558-
* NOT for the shield's own arm sites: those call setOpacity(0) and then clear
2559-
* the previous timer, so flushing there would undo the shield they just set.
2560-
*/
2561-
private cancelOpacityShield(): void {
2562-
if (!this.opacityTimeout) return;
2563-
clearTimeout(this.opacityTimeout);
2564-
this.opacityTimeout = null;
2565-
const shielded = [
2566-
this.launcherWindow,
2567-
this.overlayWindow,
2568-
this.pillWindow,
2569-
this.toggleWindow,
2570-
];
2571-
for (const win of shielded) {
2572-
if (win && !win.isDestroyed() && win.isVisible()) {
2573-
win.setOpacity(1);
2574-
}
2575-
}
2576-
}
2577-
25782512
public minimizeWindow(): void {
25792513
const win = this.launcherWindow;
25802514
if (!win || win.isDestroyed()) return;
2581-
this.cancelOpacityShield();
2515+
if (this.opacityTimeout) clearTimeout(this.opacityTimeout);
25822516
win.minimize();
25832517
}
25842518

@@ -2834,10 +2768,7 @@ export class WindowHelper {
28342768
public closeWindow(): void {
28352769
const win = this.launcherWindow;
28362770
if (!win || win.isDestroyed()) return;
2837-
// Flush, don't drop: on Windows/Linux this hides to tray rather than
2838-
// quitting, so a discarded shield restore would strand the overlay chrome
2839-
// at opacity 0 for the rest of the session.
2840-
this.cancelOpacityShield();
2771+
if (this.opacityTimeout) clearTimeout(this.opacityTimeout);
28412772
// On Windows/Linux the 'close' event listener intercepts this
28422773
// and hides to tray unless the app is actually quitting.
28432774
win.close();

electron/audio/__tests__/OpacityShieldCancelRestores.test.mjs

Lines changed: 0 additions & 182 deletions
This file was deleted.

electron/audio/__tests__/OverlayAlwaysContentProtected.test.mjs

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ test('applyContentProtection maps at least two window groups (chrome + followers
123123
});
124124

125125
for (const win of OVERLAY_CHROME) {
126-
test(`applyContentProtection protects ${win} with a literal true, never \`enable\``, () => {
126+
test(`applyContentProtection protects ${win} unconditionally (setContentProtection(true))`, () => {
127127
const owning = groups.filter((g) => g.members.includes(win));
128128
assert.ok(
129129
owning.length > 0,
@@ -135,38 +135,14 @@ for (const win of OVERLAY_CHROME) {
135135
g.arg,
136136
'true',
137137
`BUG: ${win} receives \`setContentProtection(${g.arg})\` in applyContentProtection, ` +
138-
`not \`true\`. The overlay chrome's protection must not depend on the mode — coupling it to ` +
138+
`not \`true\`. The overlay chrome must be protected unconditionally — coupling it to ` +
139139
`\`enable\` (undetectable mode) re-exposes the overlay in screen shares whenever ` +
140140
`undetectable mode is off (its default).`,
141141
);
142142
}
143143
});
144144
}
145145

146-
test('the chrome re-push in applyContentProtection still runs on macOS', () => {
147-
// The chrome loop is skipped on win32 (there the write is always
148-
// value-identical, and identical writes cause the DWM affinity churn
149-
// SetContentProtectionDedupe.test.mjs exists to prevent). It must NOT be
150-
// skipped off win32: reassertContentProtection() routes through here after
151-
// app.dock.hide()/show(), and that macOS activation-policy flip makes
152-
// WindowServer re-evaluate each NSWindow and silently reset its sharingType.
153-
// The in-memory value is still correct, so only an unconditional re-push
154-
// restores NSWindowSharingNone. Narrowing this gate to exclude darwin —
155-
// or dropping the chrome from applyContentProtection altogether — silently
156-
// un-protects the overlay after every dock toggle.
157-
const beforeChrome = body.slice(0, body.indexOf('const overlayChrome'));
158-
const gates = beforeChrome.match(/process\.platform\s*[!=]==\s*'[a-z0-9]+'/g) ?? [];
159-
for (const gate of gates) {
160-
assert.equal(
161-
gate,
162-
"process.platform !== 'win32'",
163-
`BUG: the overlay-chrome re-push in applyContentProtection is gated on \`${gate}\`. ` +
164-
`It must run on darwin — reassertContentProtection() relies on it to restore ` +
165-
`NSWindowSharingNone after app.dock.hide()/show() resets sharingType.`,
166-
);
167-
}
168-
});
169-
170146
test('applyContentProtection keeps the launcher on the undetectable-mode toggle (enable)', () => {
171147
// Sanity that the split is real: the launcher is NOT meeting chrome and must
172148
// still follow the toggle, so we are not just blanket-forcing everything true.

0 commit comments

Comments
 (0)