Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure that NVDA can get file version information for binaries in system32 on 64-bit versions of Windows #12943

Merged
merged 13 commits into from
Oct 25, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions source/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ def getInstalledUserConfigPath():
configInLocalAppData = bool(winreg.QueryValueEx(k, CONFIG_IN_LOCAL_APPDATA_SUBKEY)[0])
except WindowsError:
configInLocalAppData=False
configParent=shlobj.SHGetFolderPath(0, shlobj.CSIDL_LOCAL_APPDATA if configInLocalAppData else shlobj.CSIDL_APPDATA)
configParent = shlobj.SHGetKnownFolderPath(
shlobj.FOLDERID.LocalAppData.value if configInLocalAppData else shlobj.FOLDERID.RoamingAppData.value
)
try:
return os.path.join(configParent, "nvda")
except WindowsError:
Expand All @@ -126,14 +128,6 @@ def getUserDefaultConfigPath(useInstalledPathIfExists=False):
return installedUserConfigPath
return os.path.join(globalVars.appDir, 'userConfig')

def getSystemConfigPath():
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is likely to break backwards compat. I don't think that's a problem when this will be 2022.1

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've mentioned that consideration in the PR description. Given that we're branching for 2021.3 already this should not be a problem. Could you add this PR to the 2022.1 milestone so it is not forgotten?

Copy link
Collaborator

Choose a reason for hiding this comment

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

thanks, I must have missed that.

if isInstalledCopy():
try:
return os.path.join(shlobj.SHGetFolderPath(0, shlobj.CSIDL_COMMON_APPDATA), "nvda")
except WindowsError:
pass
return None


SCRATCH_PAD_ONLY_DIRS = (
'appModules',
Expand Down
39 changes: 34 additions & 5 deletions source/fileUtils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
#fileUtils.py
#A part of NonVisual Desktop Access (NVDA)
#Copyright (C) 2017-2019 NV Access Limited, Bram Duvigneau
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2017-2021 NV Access Limited, Bram Duvigneau, Łukasz Golonka
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

import os
import ctypes
Expand All @@ -13,6 +12,10 @@
from logHandler import log
from six import text_type
import winKernel
import shlobj
from functools import wraps
import systemUtils


@contextmanager
def FaultTolerantFile(name):
Expand Down Expand Up @@ -40,6 +43,32 @@ def FaultTolerantFile(name):
f.close()
winKernel.moveFileEx(f.name, name, winKernel.MOVEFILE_REPLACE_EXISTING)


def _suspendWow64RedirectionForFileInfoRetrieval(func):
"""
This decorator checks if the file provided as a `filePath`
is placed in a system32 directory, and if for the current system system32
redirects 32-bit processes such as NVDA to a different syswow64 directory
disables redirection for the duration of the function call.
This is necessary when fetching file version info since NVDA is a 32-bit application
and without redirection disabled we would either access a wrong file or not be able to access it at all.
"""
@wraps(func)
def funcWrapper(filePath, *attributes):
nativeSys32 = shlobj.SHGetKnownFolderPath(shlobj.FOLDERID.System.value)
if (
systemUtils.hasSyswow64Dir()
# `os.path.commonpath` is necessary to perform case-insensitive comparisons
and os.path.commonpath([nativeSys32]) == os.path.commonpath([nativeSys32, filePath])
):
with winKernel.suspendWow64Redirection():
return func(filePath, *attributes)
else:
return func(filePath, *attributes)
return funcWrapper


@_suspendWow64RedirectionForFileInfoRetrieval
def getFileVersionInfo(name, *attributes):
"""Gets the specified file version info attributes from the provided file."""
if not isinstance(name, text_type):
Expand Down
77 changes: 47 additions & 30 deletions source/shlobj.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,56 @@
# -*- coding: UTF-8 -*-
#shlobj.py
#A part of NonVisual Desktop Access (NVDA)
#Copyright (C) 2006-2017 NV Access Limited, Babbage B.V.
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2009-2021 NV Access Limited, Babbage B.V., Łukasz Golonka
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

"""
This module wraps the SHGetFolderPath function in shell32.dll and defines the necessary contstants.
CSIDL (constant special item ID list) values provide a unique system-independent way to
r"""
This module wraps the `SHGetKnownFolderPath` function in shell32.dll and defines the necessary contstants.
Known folder ids provide a unique system-independent way to
identify special folders used frequently by applications, but which may not have the same name
or location on any given system. For example, the system folder may be "C:\Windows" on one system
and "C:\Winnt" on another. The CSIDL system is used to be compatible with Windows XP.
and "C:\Winnt" on another.
"""

from ctypes import *
from ctypes.wintypes import *
import comtypes
import ctypes
import enum
import functools
import typing

shell32 = windll.shell32

MAX_PATH = 260
class FOLDERID(str, enum.Enum):
"""Contains guids of known folders from Knownfolders.h. Full list is availabe at:
https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid"""
#: The file system directory that serves as a common repository for application-specific data.
#: A typical path is C:\Documents and Settings\username\Application Data.
RoamingAppData = "{3EB685DB-65F9-4CF6-A03A-E3EF65729F3D}"
#: The file system directory that serves as a data repository for local (nonroaming) applications.
#: A typical path is C:\Documents and Settings\username\Local Settings\Application Data.
LocalAppData = "{F1B32785-6FBA-4FCF-9D55-7B8E7F157091}"
#: The file system directory that contains application data for all users.
#: A typical path is C:\Documents and Settings\All Users\Application Data.
#: This folder is used for application data that is not user specific.
ProgramData = "{62AB5D82-FDC1-4DC3-A9DD-070D1D495D97}"
# The Windows System folder.
# A typical path is C:\Windows\System32.
System = "{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}"
SystemX86 = "{D65231B0-B2F1-4857-A4CE-A8E7C6EA7D27}"

#: The file system directory that serves as a common repository for application-specific data.
#: A typical path is C:\Documents and Settings\username\Application Data.
CSIDL_APPDATA = 0x001a
#: The file system directory that serves as a data repository for local (nonroaming) applications.
#: A typical path is C:\Documents and Settings\username\Local Settings\Application Data.
CSIDL_LOCAL_APPDATA = 0x001c
#: The file system directory that contains application data for all users.
#: A typical path is C:\Documents and Settings\All Users\Application Data.
#: This folder is used for application data that is not user specific.
CSIDL_COMMON_APPDATA = 0x0023

def SHGetFolderPath(owner, folder, token=0, flags=0):
path = create_unicode_buffer(MAX_PATH)
# Note  As of Windows Vista, this function is merely a wrapper for SHGetKnownFolderPath
if shell32.SHGetFolderPathW(owner, folder, token, flags, byref(path)) != 0:
raise WinError()
return path.value
@functools.lru_cache(maxsize=128)
def SHGetKnownFolderPath(folderGuid: str, dwFlags: int = 0, hToken: typing.Optional[int] = None) -> str:
"""Wrapper for `SHGetKnownFolderPath` which caches the results
to avoid calling the win32 function unnecessarily."""
guid = comtypes.GUID(folderGuid)
pathPointer = ctypes.c_wchar_p()
res = ctypes.windll.shell32.SHGetKnownFolderPath(
comtypes.byref(guid),
dwFlags,
hToken,
ctypes.byref(pathPointer)
)
if res != 0:
raise RuntimeError(f"SHGetKnownFolderPath failed with erro code {res}")
path = pathPointer.value
ctypes.windll.ole32.CoTaskMemFree(pathPointer)
return path
11 changes: 11 additions & 0 deletions source/systemUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@
import shellapi
import winUser
import os
import functools
import shlobj


@functools.lru_cache(maxsize=1)
def hasSyswow64Dir() -> bool:
"""Returns `True` if the current system has separate system32 directories for 32-bit processes."""
nativeSys32 = shlobj.SHGetKnownFolderPath(shlobj.FOLDERID.System.value)
Syswow64Sys32 = shlobj.SHGetKnownFolderPath(shlobj.FOLDERID.SystemX86.value)
return nativeSys32 != Syswow64Sys32


def openUserConfigurationDirectory():
"""Opens directory containing config files for the current user"""
Expand Down
30 changes: 30 additions & 0 deletions source/winKernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,36 @@ def GetSystemPowerStatus(sps):
def getThreadLocale():
return kernel32.GetThreadLocale()


ERROR_INVALID_FUNCTION = 0x1


@contextlib.contextmanager
def suspendWow64Redirection():
"""Context manager which disables Wow64 redirection for a section of code and re-enables it afterwards"""
oldValue = LPVOID()
res = kernel32.Wow64DisableWow64FsRedirection(byref(oldValue))
if res == 0:
# Disabling redirection failed.
# This can occur if we're running on 32-bit Windows (no Wow64 redirection)
# or as a 64-bit process on 64-bit Windows (Wow64 redirection not applicable)
# In this case failure is expected and there is no reason to raise an exception.
# Inspect last error code to determine reason for the failure.
errorCode = kernel32.GetLastError()
if errorCode == ERROR_INVALID_FUNCTION: # Redirection not supported or not applicable.
redirectionDisabled = False
else:
raise WinError(errorCode)
else:
redirectionDisabled = True
try:
yield
finally:
if redirectionDisabled:
if kernel32.Wow64RevertWow64FsRedirection(oldValue) == 0:
raise WinError()


class SYSTEMTIME(ctypes.Structure):
_fields_ = (
("wYear", WORD),
Expand Down