Skip to content

Commit 7e0d98e

Browse files
authored
gh-94315: Check for DAC override capability (GH-94316)
``os.geteuid() == 0`` is not a reliable check whether the current user has the capability to bypass permission checks. Tests now probe for DAC override.
1 parent 1172172 commit 7e0d98e

File tree

6 files changed

+58
-22
lines changed

6 files changed

+58
-22
lines changed

Lib/test/support/os_helper.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ def can_chmod():
263263
else:
264264
can = stat.S_IMODE(mode1) != stat.S_IMODE(mode2)
265265
finally:
266-
os.unlink(TESTFN)
266+
unlink(TESTFN)
267267
_can_chmod = can
268268
return can
269269

@@ -278,6 +278,48 @@ def skip_unless_working_chmod(test):
278278
return test if ok else unittest.skip(msg)(test)
279279

280280

281+
# Check whether the current effective user has the capability to override
282+
# DAC (discretionary access control). Typically user root is able to
283+
# bypass file read, write, and execute permission checks. The capability
284+
# is independent of the effective user. See capabilities(7).
285+
_can_dac_override = None
286+
287+
def can_dac_override():
288+
global _can_dac_override
289+
290+
if not can_chmod():
291+
_can_dac_override = False
292+
if _can_dac_override is not None:
293+
return _can_dac_override
294+
295+
try:
296+
with open(TESTFN, "wb") as f:
297+
os.chmod(TESTFN, 0o400)
298+
try:
299+
with open(TESTFN, "wb"):
300+
pass
301+
except OSError:
302+
_can_dac_override = False
303+
else:
304+
_can_dac_override = True
305+
finally:
306+
unlink(TESTFN)
307+
308+
return _can_dac_override
309+
310+
311+
def skip_if_dac_override(test):
312+
ok = not can_dac_override()
313+
msg = "incompatible with CAP_DAC_OVERRIDE"
314+
return test if ok else unittest.skip(msg)(test)
315+
316+
317+
def skip_unless_dac_override(test):
318+
ok = can_dac_override()
319+
msg = "requires CAP_DAC_OVERRIDE"
320+
return test if ok else unittest.skip(msg)(test)
321+
322+
281323
def unlink(filename):
282324
try:
283325
_unlink(filename)

Lib/test/test_argparse.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,8 +1734,7 @@ def __eq__(self, other):
17341734
return self.name == other.name
17351735

17361736

1737-
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
1738-
"non-root user required")
1737+
@os_helper.skip_if_dac_override
17391738
class TestFileTypeW(TempDirMixin, ParserTestCase):
17401739
"""Test the FileType option/argument type for writing files"""
17411740

@@ -1757,8 +1756,8 @@ def setUp(self):
17571756
('-x - -', NS(x=eq_stdout, spam=eq_stdout)),
17581757
]
17591758

1760-
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
1761-
"non-root user required")
1759+
1760+
@os_helper.skip_if_dac_override
17621761
class TestFileTypeX(TempDirMixin, ParserTestCase):
17631762
"""Test the FileType option/argument type for writing new files only"""
17641763

@@ -1778,8 +1777,7 @@ def setUp(self):
17781777
]
17791778

17801779

1781-
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
1782-
"non-root user required")
1780+
@os_helper.skip_if_dac_override
17831781
class TestFileTypeWB(TempDirMixin, ParserTestCase):
17841782
"""Test the FileType option/argument type for writing binary files"""
17851783

@@ -1796,8 +1794,7 @@ class TestFileTypeWB(TempDirMixin, ParserTestCase):
17961794
]
17971795

17981796

1799-
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
1800-
"non-root user required")
1797+
@os_helper.skip_if_dac_override
18011798
class TestFileTypeXB(TestFileTypeX):
18021799
"Test the FileType option/argument type for writing new binary files only"
18031800

Lib/test/test_import/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -885,10 +885,9 @@ def test_import_pyc_path(self):
885885

886886
@unittest.skipUnless(os.name == 'posix',
887887
"test meaningful only on posix systems")
888-
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
889-
"due to varying filesystem permission semantics (issue #11956)")
890888
@skip_if_dont_write_bytecode
891889
@os_helper.skip_unless_working_chmod
890+
@os_helper.skip_if_dac_override
892891
@unittest.skipIf(is_emscripten, "umask is a stub")
893892
def test_unwritable_directory(self):
894893
# When the umask causes the new __pycache__ directory to be

Lib/test/test_py_compile.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,7 @@ def test_relative_path(self):
115115
self.assertTrue(os.path.exists(self.pyc_path))
116116
self.assertFalse(os.path.exists(self.cache_path))
117117

118-
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
119-
'non-root user required')
118+
@os_helper.skip_if_dac_override
120119
@unittest.skipIf(os.name == 'nt',
121120
'cannot control directory permissions on Windows')
122121
@os_helper.skip_unless_working_chmod

Lib/test/test_shutil.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -312,8 +312,7 @@ def onerror(*args):
312312

313313
@unittest.skipIf(sys.platform[:6] == 'cygwin',
314314
"This test can't be run on Cygwin (issue #1071513).")
315-
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
316-
"This test can't be run reliably as root (issue #1076467).")
315+
@os_helper.skip_if_dac_override
317316
@os_helper.skip_unless_working_chmod
318317
def test_on_error(self):
319318
self.errorState = 0
@@ -1033,8 +1032,7 @@ def _raise_on_src(fname, *, follow_symlinks=True):
10331032

10341033
@os_helper.skip_unless_symlink
10351034
@os_helper.skip_unless_xattr
1036-
@unittest.skipUnless(hasattr(os, 'geteuid') and os.geteuid() == 0,
1037-
'root privileges required')
1035+
@os_helper.skip_unless_dac_override
10381036
def test_copyxattr_symlinks(self):
10391037
# On Linux, it's only possible to access non-user xattr for symlinks;
10401038
# which in turn require root privileges. This test should be expanded
@@ -1830,8 +1828,7 @@ def test_cwd(self):
18301828
# Other platforms: shouldn't match in the current directory.
18311829
self.assertIsNone(rv)
18321830

1833-
@unittest.skipIf(hasattr(os, 'geteuid') and os.geteuid() == 0,
1834-
'non-root user required')
1831+
@os_helper.skip_if_dac_override
18351832
def test_non_matching_mode(self):
18361833
# Set the file read-only and ask for writeable files.
18371834
os.chmod(self.temp_file.name, stat.S_IREAD)
@@ -2182,11 +2179,11 @@ def test_move_dir_caseinsensitive(self):
21822179
os.rmdir(dst_dir)
21832180

21842181

2185-
@unittest.skipUnless(hasattr(os, 'geteuid') and os.geteuid() == 0
2186-
and hasattr(os, 'lchflags')
2182+
@os_helper.skip_unless_dac_override
2183+
@unittest.skipUnless(hasattr(os, 'lchflags')
21872184
and hasattr(stat, 'SF_IMMUTABLE')
21882185
and hasattr(stat, 'UF_OPAQUE'),
2189-
'root privileges required')
2186+
'requires lchflags')
21902187
def test_move_dir_permission_denied(self):
21912188
# bpo-42782: shutil.move should not create destination directories
21922189
# if the source directory cannot be removed.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Tests now check for DAC override capability instead of relying on
2+
:func:`os.geteuid`.

0 commit comments

Comments
 (0)