Skip to content

Comments

Check screen is black after activating screen curtain#12701

Closed
seanbudd wants to merge 5 commits intomasterfrom
checkScreenCurtainActive
Closed

Check screen is black after activating screen curtain#12701
seanbudd wants to merge 5 commits intomasterfrom
checkScreenCurtainActive

Conversation

@seanbudd
Copy link
Member

@seanbudd seanbudd commented Jul 30, 2021

Link to issue number:

Fixes #12708

Summary of the issue:

There is a security risk if the screen curtain fails silently. The Magnification API used by Screen Curtain does not officially support WOW64 applications like NVDA. As this is a risk with untested Windows versions, an external check would be helpful to minimise silent failures of the screen curtain.

Description of how this pull request fixes the issue:

Capture the screen and check it is fully black after initialising screen curtain.

wxWidgets uses winGDI methods, winGDI is a low level, stable API, as compared to the Magnification API:

"The Microsoft Windows graphics device interface (GDI) enables applications to use graphics and formatted text on both the video display and the printer. Windows-based applications do not access the graphics hardware directly. Instead, GDI interacts with device drivers on behalf of applications."

wxScreenDC captures all monitors.

Testing strategy:

Manual confirmation from a sighted developer is required.

  1. Test the screen curtain activates properly
  2. In the NVDA python console, change the black transformation matrix so it will fail to initialise the screen curtain properly.
  3. Test that the screen curtain doesn't start and a warning is provided to the user.

Example for Win 10:

from visionEnhancementProviders import screenCurtain
screenCurtain.TRANSFORM_BLACK.transform[1][1] = 1  # retain the green channel

Example for Win 11 (preview):

from visionEnhancementProviders import screenCurtain
screenCurtain.TRANSFORM_BLACK.transform[3][3] = 0  # change the opacity to 0

Known issues with pull request:

None

Change log entries:

None

Code Review Checklist:

  • Pull Request description is up to date.
  • Unit tests.
  • System (end to end) tests.
  • Manual testing.
  • User Documentation.
  • Change log entry.
  • Context sensitive help for GUI changes.
  • UX of all users considered:
    • Speech
    • Braille
    • Low Vision
    • Different web browsers

@seanbudd seanbudd added this to the 2021.2 milestone Jul 30, 2021
@seanbudd seanbudd requested a review from feerrenrut July 30, 2021 05:58
@seanbudd seanbudd requested a review from a team as a code owner July 30, 2021 05:58

def isScreenFullyBlack() -> bool:
"""
Uses wx to check that the screen is currently fully black by taking a screen capture and checking:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have you considered using the screenBitmap module for this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved the implementation to use that instead, thanks for highlighting this as an option.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having to iterate over this bitmap in python was considerably slower (took 4s on my machine). Using the wx method to calculate the histogram in native code takes 0.4s comparably.

@seanbudd seanbudd force-pushed the checkScreenCurtainActive branch from b7692c5 to 5f756a4 Compare August 2, 2021 03:28
@feerrenrut
Copy link
Contributor

Using complete black as the screen curtain color makes it hard to differentiate from a failed buffer grab (initialized to zero). Perhaps, this would be safer to transform the screen to a slightly off-black, and test for that color.

@seanbudd
Copy link
Member Author

seanbudd commented Aug 2, 2021

Using complete black as the screen curtain color makes it hard to differentiate from a failed buffer grab (initialized to zero). Perhaps, this would be safer to transform the screen to a slightly off-black, and test for that color.

Should this be done in another PR? I imagine this may be contentious with problems like screen burn in and OLED power saving.

@AppVeyorBot

This comment has been minimized.

@AppVeyorBot

This comment has been minimized.

@seanbudd seanbudd marked this pull request as draft August 3, 2021 00:26
@seanbudd seanbudd marked this pull request as ready for review August 3, 2021 03:18
@seanbudd seanbudd requested a review from feerrenrut August 3, 2021 03:19
@seanbudd seanbudd marked this pull request as draft August 3, 2021 04:58
@seanbudd seanbudd modified the milestones: 2021.2, 2021.3 Aug 3, 2021
@seanbudd seanbudd linked an issue Aug 3, 2021 that may be closed by this pull request
@seanbudd seanbudd removed this from the 2021.3 milestone Aug 5, 2021
@seanbudd seanbudd changed the base branch from beta to master September 8, 2021 05:28
@seanbudd seanbudd marked this pull request as ready for review September 8, 2021 05:29
@seanbudd
Copy link
Member Author

Using complete black as the screen curtain color makes it hard to differentiate from a failed buffer grab (initialized to zero). Perhaps, this would be safer to transform the screen to a slightly off-black, and test for that color.

@feerrenrut
Is it expected that a buffer grab fail would be a regular occurrence or just a random fail? I think the important part of this check is to flag if/when Windows changes the Magnification API causing the screen curtain to break.
We could even perform the check only for alpha builds. That is, if we are okay with just using this to fix and warn users when it breaks, as opposed to using it as a definitive future-proofed safety check.

Comment on lines +327 to +331
bmp: wx.Bitmap = wx.Bitmap(screenSize[0], screenSize[1])
mem = wx.MemoryDC(bmp)
mem.Blit(0, 0, screenSize[0], screenSize[1], screen, 0, 0) # copy screen over to bmp
del mem # Release bitmap
img: wx.Image = bmp.ConvertToImage()
Copy link
Member Author

@seanbudd seanbudd Sep 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@feerrenrut - at what stage is a buffer grab fail a concern?

Would a check like the following be an improvement?
Where setCanaryBits sets the bmp to some non-zero value canaryBits.
And areCanaryBitsSet checks if the bits are still set in the memory.

Suggested change
bmp: wx.Bitmap = wx.Bitmap(screenSize[0], screenSize[1])
mem = wx.MemoryDC(bmp)
mem.Blit(0, 0, screenSize[0], screenSize[1], screen, 0, 0) # copy screen over to bmp
del mem # Release bitmap
img: wx.Image = bmp.ConvertToImage()
bmp: wx.Bitmap = wx.Bitmap(screenSize[0], screenSize[1])
setCanaryBits(bmp, canaryBits)
mem = wx.MemoryDC(bmp)
assert areCanaryBitsSet(mem, canaryBits)
mem.Blit(0, 0, screenSize[0], screenSize[1], screen, 0, 0) # copy screen over to bmp
assert not areCanaryBitsSet(mem, canaryBits)
del mem # Release bitmap
img: wx.Image = bmp.ConvertToImage()

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This doesn't handle if screen is initialised wrong - which is handled by wx and we have little control over.

@feerrenrut
Copy link
Contributor

Is it expected that a buffer grab fail would be a regular occurrence or just a random fail? I think the important part of this check is to flag if/when Windows changes the Magnification API causing the screen curtain to break.

That's one failure we are aware of, but there are quite likely other ways that this could fail that we aren't aware of.

The feature should let users rely on it, so they don't need sighted assistance to confirm that screen curtain is working.

We could even perform the check only for alpha builds. That is, if we are okay with just using this to fix and warn users when it breaks, as opposed to using it as a definitive future-proofed safety check.

I don't think we could reliably guarantee this, even if we tested on all Windows versions (and updates) there may be other ways this could fail. Perhaps bad video drivers, or antivirus software.

Because this is privacy related, the feature should err on the side of caution.

@feerrenrut
Copy link
Contributor

I think it should take multiple approaches:

  • initialize the buffer with canaries and test they are no longer present.
  • take a screen grab before and after enabling the screen curtain, confirm several spots are now all black
  • use "off black" (at least while confirming screen curtain works). Eg set a transform that is "off black", screen grab, confirm, adjust transform to complete black.

To maintain performance it might be required to do this from C++ and asynchronously.

@seanbudd seanbudd marked this pull request as draft September 27, 2021 01:13
@seanbudd seanbudd added the merge-early Merge Early in a developer cycle label Jun 28, 2022
@seanbudd seanbudd added the conceptApproved Similar 'triaged' for issues, PR accepted in theory, implementation needs review. label Mar 4, 2024
@SaschaCowley
Copy link
Member

Closing in favour of #17894 17894

seanbudd pushed a commit that referenced this pull request Apr 6, 2025
Fixes #12708
Supercedes #12701

Summary of the issue:
There is a security risk if the screen curtain fails silently. The Magnification API used by Screen Curtain does not officially support WOW64 applications like NVDA. As this is a risk with untested Windows versions, an external check would be helpful to minimise silent failures of the screen curtain.

Notwithstanding this, as this is a security feature, it is better to check that it is working as expected, and inform the user if it is not.

Description of user facing changes
None.

Description of development approach
Implemented a new function , isScreenFullyBlack, in NVDAHelper/local/screenCurtain.cpp. This method:

Obtains a reference to the desktop window, which covers the entire virtual screen, and thus all monitors connected to the computer.
USES GDI to capture the screen.
USES GDIPlus to calculate a histogram of this screen capture.
While this seems like overkill, it performs significantly better on my machine than individually checking that each pixel is black, likely due to optimisations and hardware accelleration in GDIPlus.
Checks that, for each channel of the histogram, the value at 0 is the area of the screen.
Call this function after applying the fullscreen colour effect and hiding the cursor in visionEnhancementProviders.screenCurtain.ScreenCurtainProvider.__init__. If the return is False, raise RuntimeError, which disables the screen curtain and informs the user that screen curtain failed to activate.
nvdaes pushed a commit to nvdaes/nvda that referenced this pull request Apr 10, 2025
Fixes nvaccess#12708
Supercedes nvaccess#12701

Summary of the issue:
There is a security risk if the screen curtain fails silently. The Magnification API used by Screen Curtain does not officially support WOW64 applications like NVDA. As this is a risk with untested Windows versions, an external check would be helpful to minimise silent failures of the screen curtain.

Notwithstanding this, as this is a security feature, it is better to check that it is working as expected, and inform the user if it is not.

Description of user facing changes
None.

Description of development approach
Implemented a new function , isScreenFullyBlack, in NVDAHelper/local/screenCurtain.cpp. This method:

Obtains a reference to the desktop window, which covers the entire virtual screen, and thus all monitors connected to the computer.
USES GDI to capture the screen.
USES GDIPlus to calculate a histogram of this screen capture.
While this seems like overkill, it performs significantly better on my machine than individually checking that each pixel is black, likely due to optimisations and hardware accelleration in GDIPlus.
Checks that, for each channel of the histogram, the value at 0 is the area of the screen.
Call this function after applying the fullscreen colour effect and hiding the cursor in visionEnhancementProviders.screenCurtain.ScreenCurtainProvider.__init__. If the return is False, raise RuntimeError, which disables the screen curtain and informs the user that screen curtain failed to activate.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conceptApproved Similar 'triaged' for issues, PR accepted in theory, implementation needs review. merge-early Merge Early in a developer cycle

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Screen curtain check for success

5 participants