[fix] keep Windows privacy screen across display changes and validate covers

This commit is contained in:
dijunkun
2026-09-21 01:10:30 +08:00
parent 276385b816
commit fb556a763d
12 changed files with 296 additions and 134 deletions
@@ -14,6 +14,8 @@
#include "../../windows_thread_dpi.h"
#include "../privacy_band_ipc.h"
#include "../privacy_capture_exclusion.h"
#include "../privacy_topology.h"
#include "privacy_cover_renderer.h"
namespace {
@@ -24,11 +26,6 @@ constexpr DWORD kCoverStyles = WS_EX_TOPMOST | WS_EX_NOACTIVATE |
WS_EX_TRANSPARENT;
HMODULE module = nullptr;
bool SameRect(const RECT& a, const RECT& b) {
return a.left == b.left && a.top == b.top && a.right == b.right &&
a.bottom == b.bottom;
}
class Windows {
public:
explicit Windows(PrivacyBandShared* shared) : shared_(shared) {}
@@ -59,6 +56,7 @@ class Windows {
return atom_ ? Reconcile() : Fail(L"Register high-band window class");
}
bool Reconcile() {
InterlockedExchange(&shared_->topology_pending, 1);
std::vector<RECT> monitors;
if (!EnumDisplayMonitors(
nullptr, nullptr,
@@ -68,16 +66,37 @@ class Windows {
result.push_back(*rect);
return TRUE;
},
reinterpret_cast<LPARAM>(&monitors)) ||
monitors.empty())
reinterpret_cast<LPARAM>(&monitors)))
return Fail(L"Enumerate high-band displays");
bool changed = monitors.size() != windows_.size();
const bool changed = monitors.size() != windows_.size() ||
std::any_of(monitors.begin(), monitors.end(), [&](const RECT& rect) {
return std::none_of(windows_.begin(), windows_.end(),
[&](const auto& window) { return SameRect(rect, window.rect); });
});
// Mark the shared snapshot busy BEFORE replacing/destroying any HWND.
// Readers must not validate a stale handle from a previous topology.
struct PublishGuard {
PrivacyBandShared* shared;
~PublishGuard() { InterlockedIncrement(&shared->sequence); }
} publishing{shared_};
InterlockedIncrement(&shared_->sequence);
for (const auto& rect : monitors) {
const auto found =
std::find_if(windows_.begin(), windows_.end(),
[&](const auto& w) { return SameRect(w.rect, rect); });
if (found != windows_.end()) continue;
changed = true;
if (found != windows_.end()) {
// Windows can reposition a top-level window during DPI/hotplug before
// delivering WM_DISPLAYCHANGE. Restore the intended monitor bounds.
HWND hwnd = reinterpret_cast<HWND>(found->hwnd);
RECT actual{};
if ((!GetWindowRect(hwnd, &actual) || !SameRect(actual, rect) ||
!IsWindowVisible(hwnd)) &&
!SetWindowPos(hwnd, nullptr, rect.left, rect.top,
rect.right - rect.left, rect.bottom - rect.top,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW))
return Fail(L"Reposition high-band cover");
continue;
}
HWND hwnd = create_(kCoverStyles, atom_, L"CrossDesk Privacy", WS_POPUP,
rect.left, rect.top, rect.right - rect.left,
rect.bottom - rect.top, nullptr, nullptr, module,
@@ -86,7 +105,7 @@ class Windows {
windows_.push_back({reinterpret_cast<uint64_t>(hwnd), rect});
if (!SetLayeredWindowAttributes(hwnd, 0, 255, LWA_ALPHA))
return Fail(L"Make high-band cover opaque");
if (!SetWindowDisplayAffinity(hwnd, 0x11))
if (!ExcludePrivacyWindowFromCapture(hwnd))
return Fail(L"Exclude high-band cover from capture");
const BOOL yes = TRUE;
const HRESULT peek = DwmSetWindowAttribute(hwnd, DWMWA_EXCLUDED_FROM_PEEK,
@@ -114,13 +133,16 @@ class Windows {
++it;
}
}
if (!Validate()) return false;
if (changed) {
InterlockedIncrement(&shared_->sequence);
shared_->count = static_cast<uint32_t>(windows_.size());
std::copy(windows_.begin(), windows_.end(), shared_->windows);
InterlockedIncrement(&shared_->sequence);
if (!Validate()) {
// Another topology event can race enumeration/presentation. Retry while
// retaining every surviving cover; actual validation errors stay fatal.
return InterlockedCompareExchange(&shared_->error, 0, 0) == ERROR_SUCCESS;
}
// A previous reconciliation may have changed windows_ but failed geometry
// validation. Publish on every success so retries cannot leave stale HWNDs.
shared_->count = static_cast<uint32_t>(windows_.size());
std::copy(windows_.begin(), windows_.end(), shared_->windows);
InterlockedExchange(&shared_->topology_pending, 0);
return true;
}
bool Validate() {
@@ -130,13 +152,17 @@ class Windows {
DWORD band = 0, affinity = 0, flags = 0, cloaked = 0;
BYTE alpha = 0;
RECT rect{};
if (!GetWindowRect(hwnd, &rect) || !SameRect(rect, w.rect)) {
changed = true;
return false;
}
if ((GetWindowLongPtrW(hwnd, GWL_EXSTYLE) & kCoverStyles) !=
kCoverStyles ||
!get_band_(hwnd, &band) || band != kPrivacyWindowBand ||
!GetWindowDisplayAffinity(hwnd, &affinity) || affinity != 0x11 ||
!GetWindowDisplayAffinity(hwnd, &affinity) ||
affinity != kPrivacyWindowDisplayAffinity ||
!GetLayeredWindowAttributes(hwnd, nullptr, &alpha, &flags) ||
alpha != 255 || flags != LWA_ALPHA || !IsWindowVisible(hwnd) ||
!GetWindowRect(hwnd, &rect) || !SameRect(rect, w.rect) ||
FAILED(DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloaked,
sizeof(cloaked))) ||
cloaked)
@@ -16,15 +16,13 @@
#include "../windows_thread_dpi.h"
#include "privacy_backend.h"
#include "privacy_band_guard.h"
#include "privacy_capture_exclusion.h"
#include "privacy_cursor_guard.h"
#include "privacy_input_guard.h"
#include "privacy_topology.h"
#include "privacy_window_visibility.h"
#include "rd_log.h"
#ifndef WDA_EXCLUDEFROMCAPTURE
#define WDA_EXCLUDEFROMCAPTURE 0x00000011
#endif
namespace crossdesk {
namespace {
@@ -46,11 +44,6 @@ bool DesktopName(HDESK desktop, wchar_t (&name)[256]) {
sizeof(name), &needed);
}
bool SameRect(const RECT& a, const RECT& b) {
return a.left == b.left && a.top == b.top && a.right == b.right &&
a.bottom == b.bottom;
}
struct MonitorEnumeration {
std::vector<RECT> rects;
bool ok = true;
@@ -141,6 +134,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
return false;
if (!band_guard_.Start(text, error)) return false;
if (!ReconcileMonitors(error)) return false;
layout_changed_ = false;
event_owner_ = this;
for (DWORD event : {EVENT_OBJECT_SHOW, EVENT_OBJECT_REORDER,
EVENT_OBJECT_LOCATIONCHANGE}) {
@@ -193,6 +187,8 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
last_obstruction_ = nullptr;
last_obstruction_log_ = 0;
layout_changed_ = false;
topology_pending_ = false;
topology_pending_since_ = 0;
coverage_dirty_ = false;
emergency_ = false;
health_error_.clear();
@@ -235,12 +231,14 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
std::string error;
if (!input_guard_.Healthy(error)) return {error, false};
}
if (windows_.empty()) return {health_error_, false};
if (band_guard_.stopped()) return {health_error_, false};
// Input messages wake the thread immediately; expensive health queries
// must retain their own cadence instead of running once per input event.
if (!layout_changed_ &&
GetTickCount64() - last_check_ < 100) {
if (coverage_dirty_ && last_health_.failure.empty()) {
// Window events only dirty the coverage check, which is cheap enough to
// run between the throttled full health passes.
if (!layout_changed_ && GetTickCount64() - last_check_ < 100) {
if (coverage_dirty_ && last_health_.failure.empty() &&
!topology_pending_) {
coverage_dirty_ = false;
last_health_ = CheckCoverage();
}
@@ -253,6 +251,21 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
}
private:
// The broker reconciles hotplug asynchronously; while it does, coverage
// cannot be verified. Preserve intent for a bounded time only: a broker that
// never settles must not keep privacy "on" without any verification.
static constexpr uint64_t kTopologyPendingLimitMs = 3000;
PrivacyHealth TopologyPending() {
const uint64_t now = GetTickCount64();
if (topology_pending_since_ == 0) topology_pending_since_ = now;
if (now - topology_pending_since_ >= kTopologyPendingLimitMs) {
return {"Privacy cover topology did not settle after a display change",
false};
}
return {{}, false, false};
}
PrivacyHealth CheckHealth() {
if (!health_error_.empty()) return {health_error_, false};
if (!band_guard_.Healthy(health_error_)) return {health_error_, false};
@@ -273,40 +286,23 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
"Exclusive fullscreen or unknown display presentation",
false};
}
MonitorEnumeration monitors;
if (!EnumDisplayMonitors(nullptr, nullptr, EnumMonitor,
reinterpret_cast<LPARAM>(&monitors)) ||
!monitors.ok) {
return {Error("EnumDisplayMonitors"), false};
}
const bool changed =
monitors.rects.size() != windows_.size() ||
std::any_of(monitors.rects.begin(), monitors.rects.end(),
[this](const RECT& rect) {
return std::none_of(windows_.begin(), windows_.end(),
[&](const auto& w) {
return SameRect(w.rect, rect);
});
});
if (changed || layout_changed_) {
layout_changed_ = false;
// Retain old covers until the monitor check completes, then release
// privacy so capture can refresh its display mapping independently.
std::string error;
if (!ReconcileMonitors(error))
health_error_ = error;
else
health_error_ =
"Display layout/DPI changed; privacy screen will turn off";
}
if (!health_error_.empty()) return {health_error_, false};
if (!ReconcileMonitors(health_error_)) return {health_error_, false};
if (topology_pending_) return TopologyPending();
BOOL composed = FALSE;
if (FAILED(DwmIsCompositionEnabled(&composed)) || !composed) {
return {
"Desktop composition stopped; privacy coverage cannot be guaranteed",
false};
}
return CheckCoverage();
auto coverage = CheckCoverage();
if (!coverage.failure.empty()) {
// The broker may replace an HWND after the snapshot above. Refresh once
// before treating a stale geometry/handle as loss of privacy coverage.
if (!ReconcileMonitors(health_error_)) return {health_error_, false};
if (topology_pending_) return TopologyPending();
coverage = CheckCoverage();
}
return coverage;
}
PrivacyHealth CheckCoverage() {
@@ -328,7 +324,7 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
&layered_flags) ||
alpha != 255 || layered_flags != LWA_ALPHA ||
!GetWindowDisplayAffinity(w.cover, &affinity) ||
affinity != WDA_EXCLUDEFROMCAPTURE ||
affinity != kPrivacyWindowDisplayAffinity ||
FAILED(DwmGetWindowAttribute(w.cover, DWMWA_CLOAKED, &cloaked,
sizeof(cloaked))) ||
cloaked) {
@@ -430,40 +426,42 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
}
bool ReconcileMonitors(std::string& error) {
const auto wait_for_broker = [&] {
topology_pending_ = true;
// Preserve intent while a healthy broker reconciles hotplug. Broker
// failure/heartbeat checks still detect real faults independently.
error.clear();
return true;
};
MonitorEnumeration monitors;
if (!EnumDisplayMonitors(nullptr, nullptr, EnumMonitor,
reinterpret_cast<LPARAM>(&monitors)) ||
!monitors.ok || monitors.rects.empty()) {
reinterpret_cast<LPARAM>(&monitors)) || !monitors.ok) {
error = Error("EnumDisplayMonitors");
return false;
}
std::vector<PrivacyBandWindow> covers;
if (!band_guard_.Windows(covers, error)) return false;
for (const RECT& rect : monitors.rects) {
auto existing =
std::find_if(windows_.begin(), windows_.end(),
[&](const auto& w) { return SameRect(w.rect, rect); });
if (existing != windows_.end()) continue;
const auto cover = std::find_if(covers.begin(), covers.end(),
[&](const auto& candidate) {
return SameRect(candidate.rect, rect);
});
if (cover == covers.end()) {
error = "High-band broker has not covered every physical monitor";
return false;
}
windows_.push_back({rect, reinterpret_cast<HWND>(cover->hwnd)});
LOG_INFO(
"Privacy high-band cover ready: bounds=({}, {})-({}, {}), "
"affinity=0x11, alpha=255",
rect.left, rect.top, rect.right, rect.bottom);
if (!band_guard_.Windows(covers, error)) {
if (GetLastError() == ERROR_RETRY) return wait_for_broker();
return false;
}
// Cover windows are owned and released by the broker.
windows_.erase(std::remove_if(windows_.begin(), windows_.end(),
[&](const auto& window) {
return std::none_of(monitors.rects.begin(), monitors.rects.end(),
[&](const RECT& rect) { return SameRect(window.rect, rect); });
}), windows_.end());
if (!MatchPrivacyTopology(monitors.rects, covers)) return wait_for_broker();
std::vector<Window> updated;
for (const auto& cover : covers) {
HWND hwnd = reinterpret_cast<HWND>(cover.hwnd);
if (std::none_of(windows_.begin(), windows_.end(), [&](const auto& old) {
return old.cover == hwnd && SameRect(old.rect, cover.rect);
})) {
LOG_INFO("Privacy: reconciled local cover without changing session state: "
"bounds=({}, {})-({}, {}), affinity=0x11",
cover.rect.left, cover.rect.top, cover.rect.right, cover.rect.bottom);
}
updated.push_back({cover.rect, hwnd});
}
windows_ = std::move(updated);
layout_changed_ = false;
topology_pending_ = false;
topology_pending_since_ = 0;
return true;
}
@@ -526,6 +524,8 @@ class WindowsPrivacyBackend final : public PrivacyBackend {
bool hotkey_ = false;
bool emergency_ = false;
bool layout_changed_ = false;
bool topology_pending_ = false;
uint64_t topology_pending_since_ = 0;
bool coverage_dirty_ = false;
uint64_t last_check_ = 0;
PrivacyHealth last_health_;
@@ -119,10 +119,12 @@ bool PrivacyBandGuard::Start(const PrivacyScreenText& text,
// Keep both images immutable throughout creation/injection and the session.
image_lock_ = CreateFileW(broker_path_.c_str(), GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, 0, nullptr);
if (image_lock_ == INVALID_HANDLE_VALUE)
return Failed("Lock privacy broker image", error);
dll_lock_ = CreateFileW(dll.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, 0, nullptr);
if (image_lock_ == INVALID_HANDLE_VALUE || dll_lock_ == INVALID_HANDLE_VALUE)
return Failed("Lock privacy broker images", error);
if (dll_lock_ == INVALID_HANDLE_VALUE)
return Failed("Lock privacy window module", error);
SECURITY_ATTRIBUTES inherit{sizeof(inherit), nullptr, TRUE};
ScopedHandle parent;
@@ -131,14 +133,14 @@ bool PrivacyBandGuard::Start(const PrivacyScreenText& text,
SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, TRUE,
0))
return Failed("Duplicate privacy parent process handle", error);
// Check each object as it is created so the reported error is its own.
stop_ = CreateEventW(&inherit, TRUE, FALSE, nullptr);
if (!stop_) return Failed("Create high-band broker stop event", error);
ready_ = CreateEventW(&inherit, TRUE, FALSE, nullptr);
if (!ready_) return Failed("Create high-band broker ready event", error);
mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, &inherit, PAGE_READWRITE,
0, sizeof(PrivacyBandShared), nullptr);
if (!stop_ || !ready_ || !mapping_) {
const auto code = GetLastError();
return Failed("Create high-band broker IPC", error, code);
}
if (!mapping_) return Failed("Create high-band broker shared memory", error);
shared_ = static_cast<PrivacyBandShared*>(MapViewOfFile(
mapping_, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(PrivacyBandShared)));
if (!shared_) {
@@ -271,25 +273,30 @@ bool PrivacyBandGuard::Healthy(std::string& error) {
bool PrivacyBandGuard::Windows(std::vector<PrivacyBandWindow>& windows,
std::string& error) {
if (!Healthy(error)) return false;
const auto retry = [&] {
error = "High-band monitor snapshot is being updated";
SetLastError(ERROR_RETRY);
return false;
};
if (InterlockedCompareExchange(&shared_->topology_pending, 0, 0)) return retry();
for (int attempt = 0; attempt < 4; ++attempt) {
const LONG before = InterlockedCompareExchange(&shared_->sequence, 0, 0);
if (before & 1) continue;
const auto count = shared_->count;
if (count == 0 || count > kPrivacyMaxMonitors) break;
if (count > kPrivacyMaxMonitors) break;
windows.assign(shared_->windows, shared_->windows + count);
if (before == InterlockedCompareExchange(&shared_->sequence, 0, 0)) {
if (before == InterlockedCompareExchange(&shared_->sequence, 0, 0) &&
!InterlockedCompareExchange(&shared_->topology_pending, 0, 0)) {
for (const auto& window : windows) {
DWORD pid = 0;
GetWindowThreadProcessId(reinterpret_cast<HWND>(window.hwnd), &pid);
if (pid != process_id())
return Failed("Invalid high-band window owner", error,
ERROR_INVALID_HANDLE);
return retry();
}
return true;
}
}
return Failed("High-band monitor snapshot is changing or invalid", error,
ERROR_RETRY);
return retry();
}
bool PrivacyBandGuard::Stop(std::string& error) {
@@ -14,7 +14,7 @@
namespace crossdesk {
constexpr uint32_t kPrivacyBandMagic = 0x43534231;
constexpr uint32_t kPrivacyBandVersion = 2;
constexpr uint32_t kPrivacyBandVersion = 3;
constexpr DWORD kPrivacyWindowBand = 18; // ZBID_ABOVELOCK_UX, current desktop.
constexpr size_t kPrivacyMaxMonitors = 64;
constexpr wchar_t kPrivacyBandBootstrap[] = L"CROSSDESK_PRIVACY_BAND_BOOTSTRAP";
@@ -36,6 +36,7 @@ struct PrivacyBandShared {
volatile LONG sequence = 0;
volatile LONG error = ERROR_IO_PENDING;
volatile LONG active = 0;
volatile LONG topology_pending = 0;
alignas(8) volatile LONG64 heartbeat = 0;
wchar_t operation[128]{};
wchar_t unlock_hint[128]{};
@@ -0,0 +1,30 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-21
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PRIVACY_CAPTURE_EXCLUSION_H_
#define _PRIVACY_CAPTURE_EXCLUSION_H_
#include <Windows.h>
namespace crossdesk {
// WDA_EXCLUDEFROMCAPTURE: show the opaque cover locally while capture receives
// the desktop behind it. WDA_MONITOR would instead produce an empty/black area.
inline constexpr DWORD kPrivacyWindowDisplayAffinity = 0x00000011;
inline bool ExcludePrivacyWindowFromCapture(HWND window) {
if (!SetWindowDisplayAffinity(window, kPrivacyWindowDisplayAffinity))
return false;
DWORD affinity = 0;
if (!GetWindowDisplayAffinity(window, &affinity)) return false;
if (affinity == kPrivacyWindowDisplayAffinity) return true;
SetLastError(ERROR_INVALID_STATE);
return false;
}
} // namespace crossdesk
#endif
@@ -0,0 +1,34 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-09-21
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PRIVACY_TOPOLOGY_H_
#define _PRIVACY_TOPOLOGY_H_
#include <algorithm>
#include <vector>
#include "privacy_band_ipc.h"
namespace crossdesk {
inline bool SameRect(const RECT& a, const RECT& b) {
return a.left == b.left && a.top == b.top && a.right == b.right &&
a.bottom == b.bottom;
}
inline bool MatchPrivacyTopology(const std::vector<RECT>& monitors,
const std::vector<PrivacyBandWindow>& covers) {
return monitors.size() == covers.size() &&
std::all_of(monitors.begin(), monitors.end(), [&](const RECT& monitor) {
return std::any_of(covers.begin(), covers.end(), [&](const auto& cover) {
return SameRect(cover.rect, monitor);
});
});
}
} // namespace crossdesk
#endif
@@ -430,7 +430,7 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
}
if (size == ScreenCapturer::kBackendReset) {
SharedCapturedCursorState().Reset();
if (privacy_) privacy_->Fail("Capture restarted; privacy screen will turn off");
// Backend recovery is not a user request to disable local privacy.
return;
}
if (data && size > 0 && w > 0 && h > 0) {
@@ -578,6 +578,8 @@ void ScreenCapturerWin::EmitCapturedFrame(
} restore_source{
std::exchange(current_frame_from_secure_desktop, from_secure_desktop)};
// Privacy is a local, capture-excluded window. Always deliver the actual
// desktop pixels, including while that window is starting or stopping.
if (native_frame) {
if (!native_output_logged_.exchange(true, std::memory_order_relaxed)) {
LOG_INFO("Windows capturer native frame output enabled (type={})",
@@ -773,8 +775,6 @@ int ScreenCapturerWin::SwitchTo(int monitor_index) {
const int ret = impl_->SwitchTo(backend_index);
if (ret == 0) {
monitor_index_.store(monitor_index, std::memory_order_relaxed);
} else if (privacy_) {
privacy_->Fail("Display switch failed; privacy screen will turn off");
}
return ret;
}