Skip to content

Commit d08fb25

Browse files
GH-87235: Make sure "python /dev/fd/9 9</path/to/script.py" works on macOS (#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.
1 parent 9c9f085 commit d08fb25

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
@@ -752,6 +752,20 @@ def test_nonexisting_script(self):
752752
self.assertIn(": can't open file ", err)
753753
self.assertNotEqual(proc.returncode, 0)
754754

755+
@unittest.skipUnless(os.path.exists('/dev/fd/0'), 'requires /dev/fd platform')
756+
def test_script_as_dev_fd(self):
757+
# GH-87235: On macOS passing a non-trivial script to /dev/fd/N can cause
758+
# problems because all open /dev/fd/N file descriptors share the same
759+
# offset.
760+
script = 'print("12345678912345678912345")'
761+
with os_helper.temp_dir() as work_dir:
762+
script_name = _make_test_script(work_dir, 'script.py', script)
763+
with open(script_name, "r") as fp:
764+
p = spawn_python(f"/dev/fd/{fp.fileno()}", close_fds=False, pass_fds=(0,1,2,fp.fileno()))
765+
out, err = p.communicate()
766+
self.assertEqual(out, b"12345678912345678912345\n")
767+
768+
755769

756770
def tearDownModule():
757771
support.reap_children()

Lib/zipimport.py

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

349349
with fp:
350+
# GH-87235: On macOS all file descriptors for /dev/fd/N share the same
351+
# file offset, reset the file offset after scanning the zipfile diretory
352+
# to not cause problems when some runs 'python3 /dev/fd/9 9<some_script'
353+
start_offset = fp.tell()
350354
try:
351-
fp.seek(-END_CENTRAL_DIR_SIZE, 2)
352-
header_position = fp.tell()
353-
buffer = fp.read(END_CENTRAL_DIR_SIZE)
354-
except OSError:
355-
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
356-
if len(buffer) != END_CENTRAL_DIR_SIZE:
357-
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
358-
if buffer[:4] != STRING_END_ARCHIVE:
359-
# Bad: End of Central Dir signature
360-
# Check if there's a comment.
361355
try:
362-
fp.seek(0, 2)
363-
file_size = fp.tell()
364-
except OSError:
365-
raise ZipImportError(f"can't read Zip file: {archive!r}",
366-
path=archive)
367-
max_comment_start = max(file_size - MAX_COMMENT_LEN -
368-
END_CENTRAL_DIR_SIZE, 0)
369-
try:
370-
fp.seek(max_comment_start)
371-
data = fp.read()
372-
except OSError:
373-
raise ZipImportError(f"can't read Zip file: {archive!r}",
374-
path=archive)
375-
pos = data.rfind(STRING_END_ARCHIVE)
376-
if pos < 0:
377-
raise ZipImportError(f'not a Zip file: {archive!r}',
378-
path=archive)
379-
buffer = data[pos:pos+END_CENTRAL_DIR_SIZE]
380-
if len(buffer) != END_CENTRAL_DIR_SIZE:
381-
raise ZipImportError(f"corrupt Zip file: {archive!r}",
382-
path=archive)
383-
header_position = file_size - len(data) + pos
384-
385-
header_size = _unpack_uint32(buffer[12:16])
386-
header_offset = _unpack_uint32(buffer[16:20])
387-
if header_position < header_size:
388-
raise ZipImportError(f'bad central directory size: {archive!r}', path=archive)
389-
if header_position < header_offset:
390-
raise ZipImportError(f'bad central directory offset: {archive!r}', path=archive)
391-
header_position -= header_size
392-
arc_offset = header_position - header_offset
393-
if arc_offset < 0:
394-
raise ZipImportError(f'bad central directory size or offset: {archive!r}', path=archive)
395-
396-
files = {}
397-
# Start of Central Directory
398-
count = 0
399-
try:
400-
fp.seek(header_position)
401-
except OSError:
402-
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
403-
while True:
404-
buffer = fp.read(46)
405-
if len(buffer) < 4:
406-
raise EOFError('EOF read where not expected')
407-
# Start of file header
408-
if buffer[:4] != b'PK\x01\x02':
409-
break # Bad: Central Dir File Header
410-
if len(buffer) != 46:
411-
raise EOFError('EOF read where not expected')
412-
flags = _unpack_uint16(buffer[8:10])
413-
compress = _unpack_uint16(buffer[10:12])
414-
time = _unpack_uint16(buffer[12:14])
415-
date = _unpack_uint16(buffer[14:16])
416-
crc = _unpack_uint32(buffer[16:20])
417-
data_size = _unpack_uint32(buffer[20:24])
418-
file_size = _unpack_uint32(buffer[24:28])
419-
name_size = _unpack_uint16(buffer[28:30])
420-
extra_size = _unpack_uint16(buffer[30:32])
421-
comment_size = _unpack_uint16(buffer[32:34])
422-
file_offset = _unpack_uint32(buffer[42:46])
423-
header_size = name_size + extra_size + comment_size
424-
if file_offset > header_offset:
425-
raise ZipImportError(f'bad local header offset: {archive!r}', path=archive)
426-
file_offset += arc_offset
427-
428-
try:
429-
name = fp.read(name_size)
356+
fp.seek(-END_CENTRAL_DIR_SIZE, 2)
357+
header_position = fp.tell()
358+
buffer = fp.read(END_CENTRAL_DIR_SIZE)
430359
except OSError:
431360
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
432-
if len(name) != name_size:
361+
if len(buffer) != END_CENTRAL_DIR_SIZE:
433362
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
434-
# On Windows, calling fseek to skip over the fields we don't use is
435-
# slower than reading the data because fseek flushes stdio's
436-
# internal buffers. See issue #8745.
363+
if buffer[:4] != STRING_END_ARCHIVE:
364+
# Bad: End of Central Dir signature
365+
# Check if there's a comment.
366+
try:
367+
fp.seek(0, 2)
368+
file_size = fp.tell()
369+
except OSError:
370+
raise ZipImportError(f"can't read Zip file: {archive!r}",
371+
path=archive)
372+
max_comment_start = max(file_size - MAX_COMMENT_LEN -
373+
END_CENTRAL_DIR_SIZE, 0)
374+
try:
375+
fp.seek(max_comment_start)
376+
data = fp.read()
377+
except OSError:
378+
raise ZipImportError(f"can't read Zip file: {archive!r}",
379+
path=archive)
380+
pos = data.rfind(STRING_END_ARCHIVE)
381+
if pos < 0:
382+
raise ZipImportError(f'not a Zip file: {archive!r}',
383+
path=archive)
384+
buffer = data[pos:pos+END_CENTRAL_DIR_SIZE]
385+
if len(buffer) != END_CENTRAL_DIR_SIZE:
386+
raise ZipImportError(f"corrupt Zip file: {archive!r}",
387+
path=archive)
388+
header_position = file_size - len(data) + pos
389+
390+
header_size = _unpack_uint32(buffer[12:16])
391+
header_offset = _unpack_uint32(buffer[16:20])
392+
if header_position < header_size:
393+
raise ZipImportError(f'bad central directory size: {archive!r}', path=archive)
394+
if header_position < header_offset:
395+
raise ZipImportError(f'bad central directory offset: {archive!r}', path=archive)
396+
header_position -= header_size
397+
arc_offset = header_position - header_offset
398+
if arc_offset < 0:
399+
raise ZipImportError(f'bad central directory size or offset: {archive!r}', path=archive)
400+
401+
files = {}
402+
# Start of Central Directory
403+
count = 0
437404
try:
438-
if len(fp.read(header_size - name_size)) != header_size - name_size:
439-
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
405+
fp.seek(header_position)
440406
except OSError:
441407
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
408+
while True:
409+
buffer = fp.read(46)
410+
if len(buffer) < 4:
411+
raise EOFError('EOF read where not expected')
412+
# Start of file header
413+
if buffer[:4] != b'PK\x01\x02':
414+
break # Bad: Central Dir File Header
415+
if len(buffer) != 46:
416+
raise EOFError('EOF read where not expected')
417+
flags = _unpack_uint16(buffer[8:10])
418+
compress = _unpack_uint16(buffer[10:12])
419+
time = _unpack_uint16(buffer[12:14])
420+
date = _unpack_uint16(buffer[14:16])
421+
crc = _unpack_uint32(buffer[16:20])
422+
data_size = _unpack_uint32(buffer[20:24])
423+
file_size = _unpack_uint32(buffer[24:28])
424+
name_size = _unpack_uint16(buffer[28:30])
425+
extra_size = _unpack_uint16(buffer[30:32])
426+
comment_size = _unpack_uint16(buffer[32:34])
427+
file_offset = _unpack_uint32(buffer[42:46])
428+
header_size = name_size + extra_size + comment_size
429+
if file_offset > header_offset:
430+
raise ZipImportError(f'bad local header offset: {archive!r}', path=archive)
431+
file_offset += arc_offset
442432

443-
if flags & 0x800:
444-
# UTF-8 file names extension
445-
name = name.decode()
446-
else:
447-
# Historical ZIP filename encoding
448433
try:
449-
name = name.decode('ascii')
450-
except UnicodeDecodeError:
451-
name = name.decode('latin1').translate(cp437_table)
452-
453-
name = name.replace('/', path_sep)
454-
path = _bootstrap_external._path_join(archive, name)
455-
t = (path, compress, data_size, file_size, file_offset, time, date, crc)
456-
files[name] = t
457-
count += 1
434+
name = fp.read(name_size)
435+
except OSError:
436+
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
437+
if len(name) != name_size:
438+
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
439+
# On Windows, calling fseek to skip over the fields we don't use is
440+
# slower than reading the data because fseek flushes stdio's
441+
# internal buffers. See issue #8745.
442+
try:
443+
if len(fp.read(header_size - name_size)) != header_size - name_size:
444+
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
445+
except OSError:
446+
raise ZipImportError(f"can't read Zip file: {archive!r}", path=archive)
447+
448+
if flags & 0x800:
449+
# UTF-8 file names extension
450+
name = name.decode()
451+
else:
452+
# Historical ZIP filename encoding
453+
try:
454+
name = name.decode('ascii')
455+
except UnicodeDecodeError:
456+
name = name.decode('latin1').translate(cp437_table)
457+
458+
name = name.replace('/', path_sep)
459+
path = _bootstrap_external._path_join(archive, name)
460+
t = (path, compress, data_size, file_size, file_offset, time, date, crc)
461+
files[name] = t
462+
count += 1
463+
finally:
464+
fp.seek(start_offset)
458465
_bootstrap._verbose_message('zipimport: found {} names in {!r}', count, archive)
459466
return files
460467

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)