From 1f769f66efe92e88e326bca463ed870314c24aa7 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Sat, 14 May 2022 10:30:58 +0200 Subject: [PATCH 1/7] gh-90473: Decrease recursion limit and skip tests on WASI Reduce recursion limit to 750. WASI has limited call stack. Mark tests that require mmap, os.pipe, or fail on musl libc. --- Include/internal/pycore_ceval.h | 8 +++++++- Lib/platform.py | 4 ++++ Lib/test/pythoninfo.py | 10 ++++++++-- Lib/test/test_compile.py | 4 +++- Lib/test/test_fileio.py | 5 ++++- Lib/test/test_largefile.py | 2 ++ Lib/test/test_logging.py | 2 ++ Lib/test/test_os.py | 6 +++++- Lib/test/test_signal.py | 6 ++++++ Lib/test/test_syntax.py | 1 + Lib/test/test_zipimport.py | 1 + .../2022-05-15-15-25-05.gh-issue-90473.MoPHYW.rst | 1 + Modules/timemodule.c | 2 +- Tools/wasm/config.site-wasm32-wasi | 15 +++++++++++++++ configure | 1 + configure.ac | 2 ++ 16 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-05-15-15-25-05.gh-issue-90473.MoPHYW.rst diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h index 8dd89c6850794a..3efd6beb035a1a 100644 --- a/Include/internal/pycore_ceval.h +++ b/Include/internal/pycore_ceval.h @@ -12,8 +12,14 @@ extern "C" { struct pyruntimestate; struct _ceval_runtime_state; +/* WASI has limited call stack. wasmtime 0.36 can handle sufficient amount of + C stack frames for little more than 750 recursions. */ #ifndef Py_DEFAULT_RECURSION_LIMIT -# define Py_DEFAULT_RECURSION_LIMIT 1000 +# ifdef __wasi__ +# define Py_DEFAULT_RECURSION_LIMIT 750 +# else +# define Py_DEFAULT_RECURSION_LIMIT 1000 +# endif #endif #include "pycore_interp.h" // PyInterpreterState.eval_frame diff --git a/Lib/platform.py b/Lib/platform.py index 3f3f25a2c92d3b..c272c407c77768 100755 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -186,6 +186,10 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384): executable = sys.executable + if not executable: + # sys.executable is not set. + return lib, version + V = _comparable_version # We use os.path.realpath() # here to work around problems with Cygwin not being diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index 28549a645b0f57..84e1c047f92191 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -545,8 +545,14 @@ def format_attr(attr, value): def collect_socket(info_add): import socket - hostname = socket.gethostname() - info_add('socket.hostname', hostname) + try: + hostname = socket.gethostname() + except OSError: + # WASI SDK 15.0 does not have gethostname(2). + if sys.platform != "wasi": + raise + else: + info_add('socket.hostname', hostname) def collect_sqlite(info_add): diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index 5a9c618786f4e2..c32c27f33b447c 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -109,7 +109,9 @@ def __getitem__(self, key): self.assertEqual(d['z'], 12) def test_extended_arg(self): - longexpr = 'x = x or ' + '-x' * 2500 + # default: 1000 * 2.5 = 2500 repetitions + repeat = int(sys.getrecursionlimit() * 2.5) + longexpr = 'x = x or ' + '-x' * repeat g = {} code = ''' def f(x): diff --git a/Lib/test/test_fileio.py b/Lib/test/test_fileio.py index e4984d3cd559e3..c26cdc028cc890 100644 --- a/Lib/test/test_fileio.py +++ b/Lib/test/test_fileio.py @@ -9,7 +9,9 @@ from weakref import proxy from functools import wraps -from test.support import cpython_only, swap_attr, gc_collect, is_emscripten +from test.support import ( + cpython_only, swap_attr, gc_collect, is_emscripten, is_wasi +) from test.support.os_helper import (TESTFN, TESTFN_UNICODE, make_bad_fd) from test.support.warnings_helper import check_warnings from collections import UserList @@ -65,6 +67,7 @@ def testAttributes(self): self.assertRaises((AttributeError, TypeError), setattr, f, attr, 'oops') + @unittest.skipIf(is_wasi, "WASI does not expose st_blksize.") def testBlksize(self): # test private _blksize attribute blksize = io.DEFAULT_BUFFER_SIZE diff --git a/Lib/test/test_largefile.py b/Lib/test/test_largefile.py index 8f6bec16200534..05424fa15c2fd6 100644 --- a/Lib/test/test_largefile.py +++ b/Lib/test/test_largefile.py @@ -156,6 +156,8 @@ def test_seekable(self): def skip_no_disk_space(path, required): def decorator(fun): def wrapper(*args, **kwargs): + if not hasattr(shutil, "disg_usage"): + raise unittest.SkipTest("requires shutil.disk_usage") if shutil.disk_usage(os.path.realpath(path)).free < required: hsize = int(required / 1024 / 1024) raise unittest.SkipTest( diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index e69afae484aa77..fd562322a76372 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -5280,6 +5280,7 @@ def test_emit_after_closing_in_write_mode(self): self.assertEqual(fp.read().strip(), '1') class RotatingFileHandlerTest(BaseFileTest): + @unittest.skipIf(support.is_wasi, "WASI does not have /dev/null.") def test_should_not_rollover(self): # If maxbytes is zero rollover never occurs rh = logging.handlers.RotatingFileHandler( @@ -5387,6 +5388,7 @@ def rotator(source, dest): rh.close() class TimedRotatingFileHandlerTest(BaseFileTest): + @unittest.skipIf(support.is_wasi, "WASI does not have /dev/null.") def test_should_not_rollover(self): # See bpo-45401. Should only ever rollover regular files fh = logging.handlers.TimedRotatingFileHandler( diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 36ad587760d701..3459a9d32b9760 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -11,7 +11,6 @@ import fractions import itertools import locale -import mmap import os import pickle import select @@ -59,6 +58,10 @@ except ImportError: INT_MAX = PY_SSIZE_T_MAX = sys.maxsize +try: + import mmap +except ImportError: + mmap = None from test.support.script_helper import assert_python_ok from test.support import unix_shell @@ -2460,6 +2463,7 @@ def test_kill_int(self): # os.kill on Windows can take an int which gets set as the exit code self._kill(100) + @unittest.skipIf(mmap is None, "requires mmap") def _kill_with_event(self, event, name): tagname = "test_os_%s" % uuid.uuid1() m = mmap.mmap(-1, 1, tagname) diff --git a/Lib/test/test_signal.py b/Lib/test/test_signal.py index 6d3b299b24ccac..6aa529b0620001 100644 --- a/Lib/test/test_signal.py +++ b/Lib/test/test_signal.py @@ -107,6 +107,10 @@ def test_interprocess_signal(self): script = os.path.join(dirname, 'signalinterproctester.py') assert_python_ok(script) + @unittest.skipUnless( + hasattr(signal, "valid_signals"), + "requires signal.valid_signals" + ) def test_valid_signals(self): s = signal.valid_signals() self.assertIsInstance(s, set) @@ -212,6 +216,7 @@ def test_invalid_fd(self): self.assertRaises((ValueError, OSError), signal.set_wakeup_fd, fd) + @unittest.skipUnless(support.has_socket_support, "needs working sockets.") def test_invalid_socket(self): sock = socket.socket() fd = sock.fileno() @@ -241,6 +246,7 @@ def test_set_wakeup_fd_result(self): self.assertEqual(signal.set_wakeup_fd(-1), -1) @unittest.skipIf(support.is_emscripten, "Emscripten cannot fstat pipes.") + @unittest.skipUnless(support.has_socket_support, "needs working sockets.") def test_set_wakeup_fd_socket_result(self): sock1 = socket.socket() self.addCleanup(sock1.close) diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index 96e5c129c6599c..94e63470c79458 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -2117,6 +2117,7 @@ def test_syntax_error_on_deeply_nested_blocks(self): self._check_error(source, "too many statically nested blocks") @support.cpython_only + @unittest.skipIf(support.is_wasi, "Exhausts WASI call stack") def test_error_on_parser_stack_overflow(self): source = "-" * 100000 + "4" for mode in ["exec", "eval", "single"]: diff --git a/Lib/test/test_zipimport.py b/Lib/test/test_zipimport.py index 85dbf4d8f68eb6..66789262dd6ca1 100644 --- a/Lib/test/test_zipimport.py +++ b/Lib/test/test_zipimport.py @@ -804,6 +804,7 @@ def testEmptyFile(self): os_helper.create_empty_file(TESTMOD) self.assertZipFailure(TESTMOD) + @unittest.skipIf(support.is_wasi, "mode 000 not supported.") def testFileUnreadable(self): os_helper.unlink(TESTMOD) fd = os.open(TESTMOD, os.O_CREAT, 000) diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-05-15-15-25-05.gh-issue-90473.MoPHYW.rst b/Misc/NEWS.d/next/Core and Builtins/2022-05-15-15-25-05.gh-issue-90473.MoPHYW.rst new file mode 100644 index 00000000000000..1f9f45a511fbae --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-05-15-15-25-05.gh-issue-90473.MoPHYW.rst @@ -0,0 +1 @@ +Decrease default recursion limit on WASI to address limited call stack size. diff --git a/Modules/timemodule.c b/Modules/timemodule.c index 7475ef344b72b2..18f9ddb909c028 100644 --- a/Modules/timemodule.c +++ b/Modules/timemodule.c @@ -1481,7 +1481,7 @@ _PyTime_GetThreadTimeWithInfo(_PyTime_t *tp, _Py_clock_info_t *info) #elif defined(HAVE_CLOCK_GETTIME) && \ defined(CLOCK_PROCESS_CPUTIME_ID) && \ - !defined(__EMSCRIPTEN__) + !defined(__EMSCRIPTEN__) && !defined(__wasi__) #define HAVE_THREAD_TIME #if defined(__APPLE__) && defined(__has_attribute) && __has_attribute(availability) diff --git a/Tools/wasm/config.site-wasm32-wasi b/Tools/wasm/config.site-wasm32-wasi index 255e99c279a0a3..9e6e83bae3465f 100644 --- a/Tools/wasm/config.site-wasm32-wasi +++ b/Tools/wasm/config.site-wasm32-wasi @@ -17,3 +17,18 @@ ac_cv_header_sys_resource_h=no # undefined symbols / unsupported features ac_cv_func_eventfd=no + +# WASI SDK 15.0 has no pipe syscall +ac_cv_func_pipe=no + +# fdopendir() fails on SDK 15.0 +# OSError: [Errno 28] Invalid argument: '.' +ac_cv_func_fdopendir=no + +# WASIX stubs we don't want to use. +ac_cv_func_kill=no + +# WASI sockets are limited to operations on given socket fd and inet sockets. +# Disable AF_UNIX and AF_PACKET support, see socketmodule.h. +ac_cv_header_sys_un_h=no +ac_cv_header_netpacket_packet_h=no diff --git a/configure b/configure index 02810880e914f9..6fa4051310b124 100755 --- a/configure +++ b/configure @@ -22611,6 +22611,7 @@ case $ac_sys_system in #( py_cv_module__ctypes_test=n/a + py_cv_module_fcntl=n/a py_cv_module_=n/a diff --git a/configure.ac b/configure.ac index eab326232b14d7..bef4904325b521 100644 --- a/configure.ac +++ b/configure.ac @@ -6690,8 +6690,10 @@ AS_CASE([$ac_sys_system], ], [Emscripten/node*], [], [WASI/*], [ + dnl WASI SDK 15.0 does not support file locking. PY_STDLIB_MOD_SET_NA( [_ctypes_test], + [fcntl], ) ] ) From 61c6f26349ad4127e7def6d22730c18df908d4fa Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Tue, 17 May 2022 11:44:35 +0200 Subject: [PATCH 2/7] mkfifo and mknod aren't working either --- Lib/test/test_os.py | 3 ++- Tools/wasm/config.site-wasm32-wasi | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 3459a9d32b9760..ae071821e1ccfa 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -2170,7 +2170,8 @@ def test_fchown(self): @unittest.skipUnless(hasattr(os, 'fpathconf'), 'test needs os.fpathconf()') @unittest.skipIf( - support.is_emscripten, "musl libc issue on Emscripten, bpo-46390" + support.is_emscripten or support.is_wasi, + "musl libc issue on Emscripten/WASI, bpo-46390" ) def test_fpathconf(self): self.check(os.pathconf, "PC_NAME_MAX") diff --git a/Tools/wasm/config.site-wasm32-wasi b/Tools/wasm/config.site-wasm32-wasi index 9e6e83bae3465f..ee3fc830e3d8a3 100644 --- a/Tools/wasm/config.site-wasm32-wasi +++ b/Tools/wasm/config.site-wasm32-wasi @@ -18,10 +18,16 @@ ac_cv_header_sys_resource_h=no # undefined symbols / unsupported features ac_cv_func_eventfd=no -# WASI SDK 15.0 has no pipe syscall +# WASI SDK 15.0 has no pipe syscall. ac_cv_func_pipe=no -# fdopendir() fails on SDK 15.0 +# WASI SDK 15.0 cannot create fifos and special files. +ac_cv_func_mkfifo=no +ac_cv_func_mkfifoat=no +ac_cv_func_mknod=no +ac_cv_func_mknodat=no + +# fdopendir() fails on SDK 15.0, # OSError: [Errno 28] Invalid argument: '.' ac_cv_func_fdopendir=no From 9d6a9e9bce52f63db7157b5939a72676a0ce44c9 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Wed, 18 May 2022 10:16:38 +0200 Subject: [PATCH 3/7] Reduce parser stack size on WASI --- Lib/test/test_syntax.py | 1 - Parser/parser.c | 6 +++++- Tools/peg_generator/pegen/c_generator.py | 6 +++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index 94e63470c79458..96e5c129c6599c 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -2117,7 +2117,6 @@ def test_syntax_error_on_deeply_nested_blocks(self): self._check_error(source, "too many statically nested blocks") @support.cpython_only - @unittest.skipIf(support.is_wasi, "Exhausts WASI call stack") def test_error_on_parser_stack_overflow(self): source = "-" * 100000 + "4" for mode in ["exec", "eval", "single"]: diff --git a/Parser/parser.c b/Parser/parser.c index adc8d509eb7d7d..08bf6d2945600d 100644 --- a/Parser/parser.c +++ b/Parser/parser.c @@ -7,7 +7,11 @@ # define D(x) #endif -# define MAXSTACK 6000 +#ifdef __wasi__ +# define MAXSTACK 4000 +#else +# define MAXSTACK 6000 +#endif static const int n_keyword_lists = 9; static KeywordToken *reserved_keywords[] = { (KeywordToken[]) {{NULL, -1}}, diff --git a/Tools/peg_generator/pegen/c_generator.py b/Tools/peg_generator/pegen/c_generator.py index 56a1e5a5a14fb0..65bfd5900a6961 100644 --- a/Tools/peg_generator/pegen/c_generator.py +++ b/Tools/peg_generator/pegen/c_generator.py @@ -37,7 +37,11 @@ # define D(x) #endif -# define MAXSTACK 6000 +#ifdef __wasi__ +# define MAXSTACK 4000 +#else +# define MAXSTACK 6000 +#endif """ From f05c396b56ad9a8c6fb17257674be77960e72ff6 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Wed, 18 May 2022 10:16:48 +0200 Subject: [PATCH 4/7] Document WASI restrictions --- Tools/wasm/README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Tools/wasm/README.md b/Tools/wasm/README.md index 83806f0581ace3..977b2bb2a8ab93 100644 --- a/Tools/wasm/README.md +++ b/Tools/wasm/README.md @@ -220,10 +220,27 @@ AddType application/wasm wasm # WASI (wasm32-wasi) -WASI builds require [WASI SDK](https://github.com/WebAssembly/wasi-sdk) and -currently [wasix](https://github.com/singlestore-labs/wasix) for POSIX +WASI builds require [WASI SDK](https://github.com/WebAssembly/wasi-sdk) 15.0+ +and currently [wasix](https://github.com/singlestore-labs/wasix) for POSIX compatibility stubs. +## WASI limitations and issues (WASI SDK 15.0) + +A lot of Emscripten limitations also apply to WASI. Noticable restrictions +are: + +- Call stack size is limited. Default recursion limit and parser stack size + are smaller than in regular Python builds. +- ``socket(2)`` cannot create new socket file descriptors. WASI programs can + call read/write/accept on a file descriptor that is passed into the process. +- ``socket.gethostname()`` and host name resolution APIs like + ``socket.gethostbyname()`` are not implemented and always fail. +- ``chmod(2)`` is not available. It's not possible to modify file permissions, + yet. A future version of WASI may provide a limited ``set_permissions`` API. +- File locking (``fcntl``) is not available. +- ``os.pipe()``, ``os.mkfifo()``, and ``os.mknod()`` are not supported. + + # Detect WebAssembly builds ## Python code From 3b6ca174bfac9ed9dd6319fa2d3a6e9b36e92121 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Wed, 18 May 2022 10:42:34 +0200 Subject: [PATCH 5/7] fix typo --- Lib/test/test_largefile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_largefile.py b/Lib/test/test_largefile.py index 05424fa15c2fd6..3c11c59baef6e5 100644 --- a/Lib/test/test_largefile.py +++ b/Lib/test/test_largefile.py @@ -156,7 +156,7 @@ def test_seekable(self): def skip_no_disk_space(path, required): def decorator(fun): def wrapper(*args, **kwargs): - if not hasattr(shutil, "disg_usage"): + if not hasattr(shutil, "disk_usage"): raise unittest.SkipTest("requires shutil.disk_usage") if shutil.disk_usage(os.path.realpath(path)).free < required: hsize = int(required / 1024 / 1024) From e67eab4008dc5fb3968e169e6d64b9f134b5c2da Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Wed, 18 May 2022 15:35:36 +0200 Subject: [PATCH 6/7] Define nest count in relation to recursion limit --- Lib/test/test_tomllib/test_misc.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_tomllib/test_misc.py b/Lib/test/test_tomllib/test_misc.py index 76fa5905fa49ef..378db58f255945 100644 --- a/Lib/test/test_tomllib/test_misc.py +++ b/Lib/test/test_tomllib/test_misc.py @@ -6,6 +6,7 @@ import datetime from decimal import Decimal as D from pathlib import Path +import sys import tempfile import unittest @@ -91,11 +92,13 @@ def test_deepcopy(self): self.assertEqual(obj_copy, expected_obj) def test_inline_array_recursion_limit(self): - nest_count = 470 + # 470 with default recursion limit + nest_count = int(sys.getrecursionlimit() * 0.47) recursive_array_toml = "arr = " + nest_count * "[" + nest_count * "]" tomllib.loads(recursive_array_toml) def test_inline_table_recursion_limit(self): - nest_count = 310 + # 310 with default recursion limit + nest_count = int(sys.getrecursionlimit() * 0.31) recursive_table_toml = nest_count * "key = {" + nest_count * "}" tomllib.loads(recursive_table_toml) From bd029488a2d9c32e7ed97ca22cea44fbc9758ece Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Wed, 18 May 2022 16:33:34 +0200 Subject: [PATCH 7/7] chmod() won't fix a missing file --- Lib/test/support/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index e4bda940b3dd60..c284fc67b64026 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -199,6 +199,11 @@ def get_original_stdout(): def _force_run(path, func, *args): try: return func(*args) + except FileNotFoundError as err: + # chmod() won't fix a missing file. + if verbose >= 2: + print('%s: %s' % (err.__class__.__name__, err)) + raise except OSError as err: if verbose >= 2: print('%s: %s' % (err.__class__.__name__, err))