Skip to content

Commit d3ecdba

Browse files
committed
Adjustments after revert
NOTE: This needs a full buildbot test pass before merge, see: #121143 (comment). 1. Added `statx` to set of allowed syscall forms (Should make Raspian bot pass). 2. Check that the `fd` returned from `open` is passed to all future calls. This helps ensure things like the `stat` call uses the file descriptor rather than the `filename` to avoid TOCTOU isuses. 3. Update the `Path().read_bytes()` test case to additionally validate the reduction in`isatty`/`ioctl` + `seek` calls from #122111 4. Better diagnostic assertion messagess from @gpshead, so when the test fails have first information immediately available. Makes remote CI debugging much simpler.
1 parent aff5270 commit d3ecdba

File tree

2 files changed

+65
-17
lines changed

2 files changed

+65
-17
lines changed

Lib/test/support/strace_helper.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def _make_error(reason, details):
121121
stderr=res.err)
122122

123123

124-
def _get_events(code, strace_flags, prelude, cleanup):
124+
def get_events(code, strace_flags, prelude, cleanup):
125125
# NOTE: The flush is currently required to prevent the prints from getting
126126
# buffered and done all at once at exit
127127
prelude = textwrap.dedent(prelude)
@@ -143,7 +143,7 @@ def _get_events(code, strace_flags, prelude, cleanup):
143143

144144
def get_syscalls(code, strace_flags, prelude="", cleanup=""):
145145
"""Get the syscalls which a given chunk of python code generates"""
146-
events = _get_events(code, strace_flags, prelude=prelude, cleanup=cleanup)
146+
events = get_events(code, strace_flags, prelude=prelude, cleanup=cleanup)
147147
return [ev.syscall for ev in events]
148148

149149

Lib/test/test_fileio.py

+63-15
Original file line numberDiff line numberDiff line change
@@ -376,25 +376,62 @@ def test_syscalls_read(self):
376376
self.f.close()
377377

378378

379-
def check_readall(name, code, prelude="", cleanup=""):
379+
def check_readall(name, code, prelude="", cleanup="",
380+
extra_checks=None):
380381
with self.subTest(name=name):
381-
syscalls = strace_helper.get_syscalls(code, _strace_flags,
382+
syscalls = strace_helper.get_events(code, _strace_flags,
382383
prelude=prelude,
383384
cleanup=cleanup)
384385

386+
# The first call should be an open that returns a
387+
# file descriptor (fd). Afer that calls may vary. Once the file
388+
# is opened, check calls refer to it by fd as the filename
389+
# could be removed from the filesystem, renamed, etc. See:
390+
# Time-of-check time-of-use (TOCTOU) software bug class.
391+
#
392+
# There are a number of related but distinct open system calls
393+
# so not checking precise name here.
394+
self.assertGreater(
395+
len(syscalls),
396+
1,
397+
f"Should have had at least an open call|calls={syscalls}")
398+
fd_str = syscalls[0].returncode
399+
400+
# All other calls should contain the fd in their argument set.
401+
for ev in syscalls[1:]:
402+
self.assertIn(
403+
fd_str,
404+
ev.args,
405+
f"Looking for file descriptor in arguments|ev={ev}"
406+
)
407+
385408
# There are a number of related syscalls used to implement
386-
# behaviors in a libc (ex. fstat, newfstatat, open, openat).
409+
# behaviors in a libc (ex. fstat, newfstatat, statx, open, openat).
387410
# Allow any that use the same substring.
388411
def count_similarname(name):
389-
return len([sc for sc in syscalls if name in sc])
390-
391-
# Should open and close the file exactly once
392-
self.assertEqual(count_similarname('open'), 1)
393-
self.assertEqual(count_similarname('close'), 1)
394-
395-
# Should only have one fstat (bpo-21679, gh-120754)
396-
self.assertEqual(count_similarname('fstat'), 1)
397-
412+
return len([ev for ev in syscalls if name in ev.syscall])
413+
414+
checks = [
415+
# Should open and close the file exactly once
416+
("open", 1),
417+
("close", 1),
418+
# Should only have one fstat (bpo-21679, gh-120754)
419+
# note: It's important this uses a fd rather than filename,
420+
# That is validated by the `fd` check above.
421+
# note: fstat, newfstatat, and statx have all been observed
422+
# here in the underlying C library implementations.
423+
("stat", 1)
424+
]
425+
426+
if extra_checks:
427+
checks += extra_checks
428+
429+
for call, count in checks:
430+
self.assertEqual(
431+
count_similarname(call),
432+
count,
433+
msg=f"call={call}|count={count}|syscalls={syscalls}"
434+
)
398435

399436
# "open, read, close" file using different common patterns.
400437
check_readall(
@@ -421,14 +458,25 @@ def count_similarname(name):
421458
f = open('{TESTFN}', 'rt')
422459
f.read()
423460
f.close()
424-
"""
461+
""",
462+
# GH-122111: read_text uses BufferedIO which requires looking up
463+
# position in file. `read_bytes` disables that buffering, checked
464+
# next and avoid these calls.
465+
extra_checks=[
466+
("ioctl", 1),
467+
("seek", 1)
468+
]
425469
)
426470

427471
check_readall(
428472
"pathlib read_bytes",
429473
"p.read_bytes()",
430-
prelude=f"""from pathlib import Path; p = Path("{TESTFN}")"""
431-
474+
prelude=f"""from pathlib import Path; p = Path("{TESTFN}")""",
475+
# GH-122111: Buffering is disabled so these calls are avoided.
476+
extra_checks=[
477+
("ioctl", 0),
478+
("seek", 0)
479+
]
432480
)
433481

434482
check_readall(

0 commit comments

Comments
 (0)