Skip to content

Commit a8dc6d6

Browse files
moreatijaracobrettcannon
authored
gh-115911: Ignore PermissionError during import from cwd (#116131)
Ignore PermissionError when checking cwd during import On macOS `getcwd(3)` can return EACCES if a path component isn't readable, resulting in PermissionError. `PathFinder.find_spec()` now catches these and ignores them - the same treatment as a missing/deleted cwd. Introduces `test.support.os_helper.save_mode(path, ...)`, a context manager that restores the mode of a path on exit. This is allows finer control of exception handling and robust environment restoration across platforms in `FinderTests.test_permission_error_cwd()`. Co-authored-by: Jason R. Coombs <[email protected]> Co-authored-by: Brett Cannon <[email protected]>
1 parent 914c232 commit a8dc6d6

File tree

5 files changed

+58
-5
lines changed

5 files changed

+58
-5
lines changed

Doc/reference/import.rst

+4-4
Original file line numberDiff line numberDiff line change
@@ -762,10 +762,10 @@ module.
762762

763763
The current working directory -- denoted by an empty string -- is handled
764764
slightly differently from other entries on :data:`sys.path`. First, if the
765-
current working directory is found to not exist, no value is stored in
766-
:data:`sys.path_importer_cache`. Second, the value for the current working
767-
directory is looked up fresh for each module lookup. Third, the path used for
768-
:data:`sys.path_importer_cache` and returned by
765+
current working directory cannot be determined or is found not to exist, no
766+
value is stored in :data:`sys.path_importer_cache`. Second, the value for the
767+
current working directory is looked up fresh for each module lookup. Third,
768+
the path used for :data:`sys.path_importer_cache` and returned by
769769
:meth:`importlib.machinery.PathFinder.find_spec` will be the actual current
770770
working directory and not the empty string.
771771

Lib/importlib/_bootstrap_external.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1244,7 +1244,7 @@ def _path_importer_cache(cls, path):
12441244
if path == '':
12451245
try:
12461246
path = _os.getcwd()
1247-
except FileNotFoundError:
1247+
except (FileNotFoundError, PermissionError):
12481248
# Don't cache the failure as the cwd can easily change to
12491249
# a valid directory later on.
12501250
return None

Lib/test/support/os_helper.py

+27
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,33 @@ def skip_unless_working_chmod(test):
294294
return test if ok else unittest.skip(msg)(test)
295295

296296

297+
@contextlib.contextmanager
298+
def save_mode(path, *, quiet=False):
299+
"""Context manager that restores the mode (permissions) of *path* on exit.
300+
301+
Arguments:
302+
303+
path: Path of the file to restore the mode of.
304+
305+
quiet: if False (the default), the context manager raises an exception
306+
on error. Otherwise, it issues only a warning and keeps the current
307+
working directory the same.
308+
309+
"""
310+
saved_mode = os.stat(path)
311+
try:
312+
yield
313+
finally:
314+
try:
315+
os.chmod(path, saved_mode.st_mode)
316+
except OSError as exc:
317+
if not quiet:
318+
raise
319+
warnings.warn(f'tests may fail, unable to restore the mode of '
320+
f'{path!r} to {saved_mode.st_mode}: {exc}',
321+
RuntimeWarning, stacklevel=3)
322+
323+
297324
# Check whether the current effective user has the capability to override
298325
# DAC (discretionary access control). Typically user root is able to
299326
# bypass file read, write, and execute permission checks. The capability

Lib/test/test_importlib/import_/test_path.py

+23
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from test.support import os_helper
12
from test.test_importlib import util
23

34
importlib = util.import_importlib('importlib')
@@ -153,6 +154,28 @@ def test_deleted_cwd(self):
153154
# Do not want FileNotFoundError raised.
154155
self.assertIsNone(self.machinery.PathFinder.find_spec('whatever'))
155156

157+
@os_helper.skip_unless_working_chmod
158+
def test_permission_error_cwd(self):
159+
# gh-115911: Test that an unreadable CWD does not break imports, in
160+
# particular during early stages of interpreter startup.
161+
with (
162+
os_helper.temp_dir() as new_dir,
163+
os_helper.save_mode(new_dir),
164+
os_helper.change_cwd(new_dir),
165+
util.import_state(path=['']),
166+
):
167+
# chmod() is done here (inside the 'with' block) because the order
168+
# of teardown operations cannot be the reverse of setup order. See
169+
# https://github.com/python/cpython/pull/116131#discussion_r1739649390
170+
try:
171+
os.chmod(new_dir, 0o000)
172+
except OSError:
173+
self.skipTest("platform does not allow "
174+
"changing mode of the cwd")
175+
176+
# Do not want PermissionError raised.
177+
self.assertIsNone(self.machinery.PathFinder.find_spec('whatever'))
178+
156179
def test_invalidate_caches_finders(self):
157180
# Finders with an invalidate_caches() method have it called.
158181
class FakeFinder:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
If the current working directory cannot be determined due to permissions,
2+
then import will no longer raise :exc:`PermissionError`. Patch by Alex
3+
Willmer.

0 commit comments

Comments
 (0)