Skip to content

Commit 2136b09

Browse files
ronaldoussorenmiss-islington
authored andcommitted
pythonGH-87235: Make sure "python /dev/fd/9 9</path/to/script.py" works on macOS (pythonGH-99768)
On macOS all file descriptors for a particular file in /dev/fd share the same file offset, that is ``open("/dev/fd/9", "r")`` behaves more like ``dup(9)`` than a regular open. This causes problems when a user tries to run "/dev/fd/9" as a script because zipimport changes the file offset to try to read a zipfile directory. Therefore change zipimport to reset the file offset after trying to read the zipfile directory. (cherry picked from commit d08fb25) Co-authored-by: Ronald Oussoren <ronaldoussoren@mac.com>
1 parent 72d1735 commit 2136b09

File tree

3 files changed

+120
-98
lines changed

3 files changed

+120
-98
lines changed

Lib/test/test_cmd_line_script.py

+14
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,20 @@ def test_nonexisting_script(self):
738738
self.assertIn(": can't open file ", err)
739739
self.assertNotEqual(proc.returncode, 0)
740740

741+
@unittest.skipUnless(os.path.exists('/dev/fd/0'), 'requires /dev/fd platform')
742+
def test_script_as_dev_fd(self):
743+
# GH-87235: On macOS passing a non-trivial script to /dev/fd/N can cause
744+
# problems because all open /dev/fd/N file descriptors share the same
745+
# offset.
746+
script = 'print("12345678912345678912345")'
747+
with os_helper.temp_dir() as work_dir:
748+
script_name = _make_test_script(work_dir, 'script.py', script)
749+
with open(script_name, "r") as fp:
750+
p = spawn_python(f"/dev/fd/{fp.fileno()}", close_fds=False, pass_fds=(0,1,2,fp.fileno()))
751+
out, err = p.communicate()
752+
self.assertEqual(out, b"12345678912345678912345\n")
753+
754+
741755

742756
def tearDownModule():
743757
support.reap_children()

Lib/zipimport.py

+105-98
Original file line numberDiff line numberDiff line change
@@ -406,114 +406,121 @@ def _read_directory(archive):
406406
raise ZipImportError(f"can't open Zip file: {archive!r}", path=archive)
407407

408408
with fp:
409+
# GH-87235: On macOS all file descriptors for /dev/fd/N share the same
410+
# file offset, reset the file offset after scanning the zipfile diretory
411+
# to not cause problems when some runs 'python3 /dev/fd/9 9<some_script'
412+
start_offset = fp.tell()
409413
try:
410-
fp.seek(-END_CENTRAL_DIR_SIZE, 2)
411-
header_position = fp.tell()
412-
buffer = fp.read(END_CENTRAL_DIR_SIZE)
413-
except OSError:
414-
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
415-
if len(buffer) != END_CENTRAL_DIR_SIZE:
416-
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
417-
if buffer[:4] != STRING_END_ARCHIVE:
418-
# Bad: End of Central Dir signature
419-
# Check if there's a comment.
420414
try:
421-
fp.seek(0, 2)
422-
file_size = fp.tell()
423-
except OSError:
424-
raise ZipImportError(f"can't read Zip file: {archive!r}",
425-
path=archive)
426-
max_comment_start = max(file_size - MAX_COMMENT_LEN -
427-
END_CENTRAL_DIR_SIZE, 0)
428-
try:
429-
fp.seek(max_comment_start)
430-
data = fp.read()
431-
except OSError:
432-
raise ZipImportError(f"can't read Zip file: {archive!r}",
433-
path=archive)
434-
pos = data.rfind(STRING_END_ARCHIVE)
435-
if pos < 0:
436-
raise ZipImportError(f'not a Zip file: {archive!r}',
437-
path=archive)
438-
buffer = data[pos:pos+END_CENTRAL_DIR_SIZE]
439-
if len(buffer) != END_CENTRAL_DIR_SIZE:
440-
raise ZipImportError(f"corrupt Zip file: {archive!r}",
441-
path=archive)
442-
header_position = file_size - len(data) + pos
443-
444-
header_size = _unpack_uint32(buffer[12:16])
445-
header_offset = _unpack_uint32(buffer[16:20])
446-
if header_position < header_size:
447-
raise ZipImportError(f'bad central directory size: {archive!r}', path=archive)
448-
if header_position < header_offset:
449-
raise ZipImportError(f'bad central directory offset: {archive!r}', path=archive)
450-
header_position -= header_size
451-
arc_offset = header_position - header_offset
452-
if arc_offset < 0:
453-
raise ZipImportError(f'bad central directory size or offset: {archive!r}', path=archive)
454-
455-
files = {}
456-
# Start of Central Directory
457-
count = 0
458-
try:
459-
fp.seek(header_position)
460-
except OSError:
461-
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
462-
while True:
463-
buffer = fp.read(46)
464-
if len(buffer) < 4:
465-
raise EOFError('EOF read where not expected')
466-
# Start of file header
467-
if buffer[:4] != b'PK\x01\x02':
468-
break # Bad: Central Dir File Header
469-
if len(buffer) != 46:
470-
raise EOFError('EOF read where not expected')
471-
flags = _unpack_uint16(buffer[8:10])
472-
compress = _unpack_uint16(buffer[10:12])
473-
time = _unpack_uint16(buffer[12:14])
474-
date = _unpack_uint16(buffer[14:16])
475-
crc = _unpack_uint32(buffer[16:20])
476-
data_size = _unpack_uint32(buffer[20:24])
477-
file_size = _unpack_uint32(buffer[24:28])
478-
name_size = _unpack_uint16(buffer[28:30])
479-
extra_size = _unpack_uint16(buffer[30:32])
480-
comment_size = _unpack_uint16(buffer[32:34])
481-
file_offset = _unpack_uint32(buffer[42:46])
482-
header_size = name_size + extra_size + comment_size
483-
if file_offset > header_offset:
484-
raise ZipImportError(f'bad local header offset: {archive!r}', path=archive)
485-
file_offset += arc_offset
486-
487-
try:
488-
name = fp.read(name_size)
415+
fp.seek(-END_CENTRAL_DIR_SIZE, 2)
416+
header_position = fp.tell()
417+
buffer = fp.read(END_CENTRAL_DIR_SIZE)
489418
except OSError:
490419
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
491-
if len(name) != name_size:
420+
if len(buffer) != END_CENTRAL_DIR_SIZE:
492421
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
493-
# On Windows, calling fseek to skip over the fields we don't use is
494-
# slower than reading the data because fseek flushes stdio's
495-
# internal buffers. See issue #8745.
422+
if buffer[:4] != STRING_END_ARCHIVE:
423+
# Bad: End of Central Dir signature
424+
# Check if there's a comment.
425+
try:
426+
fp.seek(0, 2)
427+
file_size = fp.tell()
428+
except OSError:
429+
raise ZipImportError(f"can't read Zip file: {archive!r}",
430+
path=archive)
431+
max_comment_start = max(file_size - MAX_COMMENT_LEN -
432+
END_CENTRAL_DIR_SIZE, 0)
433+
try:
434+
fp.seek(max_comment_start)
435+
data = fp.read()
436+
except OSError:
437+
raise ZipImportError(f"can't read Zip file: {archive!r}",
438+
path=archive)
439+
pos = data.rfind(STRING_END_ARCHIVE)
440+
if pos < 0:
441+
raise ZipImportError(f'not a Zip file: {archive!r}',
442+
path=archive)
443+
buffer = data[pos:pos+END_CENTRAL_DIR_SIZE]
444+
if len(buffer) != END_CENTRAL_DIR_SIZE:
445+
raise ZipImportError(f"corrupt Zip file: {archive!r}",
446+
path=archive)
447+
header_position = file_size - len(data) + pos
448+
449+
header_size = _unpack_uint32(buffer[12:16])
450+
header_offset = _unpack_uint32(buffer[16:20])
451+
if header_position < header_size:
452+
raise ZipImportError(f'bad central directory size: {archive!r}', path=archive)
453+
if header_position < header_offset:
454+
raise ZipImportError(f'bad central directory offset: {archive!r}', path=archive)
455+
header_position -= header_size
456+
arc_offset = header_position - header_offset
457+
if arc_offset < 0:
458+
raise ZipImportError(f'bad central directory size or offset: {archive!r}', path=archive)
459+
460+
files = {}
461+
# Start of Central Directory
462+
count = 0
496463
try:
497-
if len(fp.read(header_size - name_size)) != header_size - name_size:
498-
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
464+
fp.seek(header_position)
499465
except OSError:
500466
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
467+
while True:
468+
buffer = fp.read(46)
469+
if len(buffer) < 4:
470+
raise EOFError('EOF read where not expected')
471+
# Start of file header
472+
if buffer[:4] != b'PK\x01\x02':
473+
break # Bad: Central Dir File Header
474+
if len(buffer) != 46:
475+
raise EOFError('EOF read where not expected')
476+
flags = _unpack_uint16(buffer[8:10])
477+
compress = _unpack_uint16(buffer[10:12])
478+
time = _unpack_uint16(buffer[12:14])
479+
date = _unpack_uint16(buffer[14:16])
480+
crc = _unpack_uint32(buffer[16:20])
481+
data_size = _unpack_uint32(buffer[20:24])
482+
file_size = _unpack_uint32(buffer[24:28])
483+
name_size = _unpack_uint16(buffer[28:30])
484+
extra_size = _unpack_uint16(buffer[30:32])
485+
comment_size = _unpack_uint16(buffer[32:34])
486+
file_offset = _unpack_uint32(buffer[42:46])
487+
header_size = name_size + extra_size + comment_size
488+
if file_offset > header_offset:
489+
raise ZipImportError(f'bad local header offset: {archive!r}', path=archive)
490+
file_offset += arc_offset
501491

502-
if flags & 0x800:
503-
# UTF-8 file names extension
504-
name = name.decode()
505-
else:
506-
# Historical ZIP filename encoding
507492
try:
508-
name = name.decode('ascii')
509-
except UnicodeDecodeError:
510-
name = name.decode('latin1').translate(cp437_table)
511-
512-
name = name.replace('/', path_sep)
513-
path = _bootstrap_external._path_join(archive, name)
514-
t = (path, compress, data_size, file_size, file_offset, time, date, crc)
515-
files[name] = t
516-
count += 1
493+
name = fp.read(name_size)
494+
except OSError:
495+
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
496+
if len(name) != name_size:
497+
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
498+
# On Windows, calling fseek to skip over the fields we don't use is
499+
# slower than reading the data because fseek flushes stdio's
500+
# internal buffers. See issue #8745.
501+
try:
502+
if len(fp.read(header_size - name_size)) != header_size - name_size:
503+
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
504+
except OSError:
505+
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
506+
507+
if flags & 0x800:
508+
# UTF-8 file names extension
509+
name = name.decode()
510+
else:
511+
# Historical ZIP filename encoding
512+
try:
513+
name = name.decode('ascii')
514+
except UnicodeDecodeError:
515+
name = name.decode('latin1').translate(cp437_table)
516+
517+
name = name.replace('/', path_sep)
518+
path = _bootstrap_external._path_join(archive, name)
519+
t = (path, compress, data_size, file_size, file_offset, time, date, crc)
520+
files[name] = t
521+
count += 1
522+
finally:
523+
fp.seek(start_offset)
517524
_bootstrap._verbose_message('zipimport: found {} names in {!r}', count, archive)
518525
return files
519526

Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
On macOS ``python3 /dev/fd/9 9</path/to/script.py`` failed for any script longer than a couple of bytes.

0 commit comments

Comments
 (0)