From 3e8a9bd6dd64c37dfd7d8fbdf1ee03b2108cf7ba Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Fri, 2 Oct 2026 12:08:28 -0700 Subject: [PATCH 1/6] gh-129040: Use wasm-gc to handle pointer casts in wasi This makes it so that in wasi builds if `-DPY_CALL_TRAMPOLINE` is passed, a call trampoline adaptor is used to prevent traps when there is a call signature mismatch between a C slot handler and the signature that the interpreter uses to call it. This requires Clang >= 22. PEP 11 specifies that builds of Python 3.15 use WASI SDK version 33 which includes clang 23.1.0, so this could be used with Python 3.15 WASI builds but not earlier ones. It requires a WebAssembly runtime that supports wasm-gc. Wasmtime has supported wasm-gc since version 27.0 released November 25, 2024. Some other runtimes still don't support it, but for those runtimes people can use builds with the trampoline disabled. I also removed the Emscripten trampoline support for JS runtimes with no wasm-gc support. This removes a lot of implementation complexity and allows WASI and Emscripten to share code. The main concern is that it drops support for some very old iPhones, but every iPhone model released since 2018 is compatible with versions of Safari that support wasm-gc. I also added tests that define handlers with various wrong numbers of arguments. The tests also run on non-webassembly platforms since they should work there as well. --- Include/internal/pycore_object.h | 23 +-- Include/internal/pycore_runtime_structs.h | 10 - ..._trampoline.h => pycore_wasm_trampoline.h} | 45 +++-- Lib/test/test_capi/test_fpcast.py | 73 ++++++++ Makefile.pre.in | 8 +- ...-10-02-12-18-20.gh-issue-129040.6wJPY6.rst | 11 ++ Modules/Setup.stdlib.in | 2 +- Modules/_testcapi/fpcast.c | 171 ++++++++++++++++++ Modules/_testcapi/parts.h | 1 + Modules/_testcapimodule.c | 3 + Objects/descrobject.c | 2 +- PCbuild/_testcapi.vcxproj | 1 + PCbuild/_testcapi.vcxproj.filters | 3 + Platforms/WASI/_build.py | 1 + Python/emscripten_trampoline.c | 136 -------------- Python/emscripten_trampoline_inner.c | 38 ---- Python/wasm_trampoline.c | 48 +++++ Tools/c-analyzer/cpython/_parser.py | 1 - configure | 13 +- configure.ac | 12 +- 20 files changed, 361 insertions(+), 241 deletions(-) rename Include/internal/{pycore_emscripten_trampoline.h => pycore_wasm_trampoline.h} (56%) create mode 100644 Lib/test/test_capi/test_fpcast.py create mode 100644 Misc/NEWS.d/next/Build/2026-10-02-12-18-20.gh-issue-129040.6wJPY6.rst create mode 100644 Modules/_testcapi/fpcast.c delete mode 100644 Python/emscripten_trampoline.c delete mode 100644 Python/emscripten_trampoline_inner.c create mode 100644 Python/wasm_trampoline.c diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h index 3ecc5e9f0554b0..dbc0ebf994fd1e 100644 --- a/Include/internal/pycore_object.h +++ b/Include/internal/pycore_object.h @@ -8,7 +8,7 @@ extern "C" { # error "this header requires Py_BUILD_CORE define" #endif -#include "pycore_emscripten_trampoline.h" // _PyCFunction_TrampolineCall() +#include "pycore_wasm_trampoline.h" // _PyCFunction_TrampolineCall() #include "pycore_gc.h" // _PyObject_GC_TRACK() #include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_PTR_ACQUIRE() #include "pycore_pystate.h" // _PyInterpreterState_GET() @@ -977,27 +977,6 @@ extern PyObject* _PyObject_NextNotImplemented(PyObject *); // Export for '_datetime' shared extension PyAPI_FUNC(PyObject*) _PyObject_GetState(PyObject *); -/* C function call trampolines to mitigate bad function pointer casts. - * - * Typical native ABIs ignore additional arguments or fill in missing - * values with 0/NULL in function pointer cast. Compilers do not show - * warnings when a function pointer is explicitly casted to an - * incompatible type. - * - * Bad fpcasts are an issue in WebAssembly. WASM's indirect_call has strict - * function signature checks. Argument count, types, and return type must - * match. - * - * Third party code unintentionally rely on problematic fpcasts. The call - * trampoline mitigates common occurrences of bad fpcasts on Emscripten. - */ -#if !(defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)) -#define _PyCFunction_TrampolineCall(meth, self, args) \ - (meth)((self), (args)) -#define _PyCFunctionWithKeywords_TrampolineCall(meth, self, args, kw) \ - (meth)((self), (args), (kw)) -#endif // __EMSCRIPTEN__ && PY_CALL_TRAMPOLINE - // Export these 2 symbols for '_pickle' shared extension PyAPI_DATA(PyTypeObject) _PyNone_Type; PyAPI_DATA(PyTypeObject) _PyNotImplemented_Type; diff --git a/Include/internal/pycore_runtime_structs.h b/Include/internal/pycore_runtime_structs.h index 145e66de9984ca..5470f5858eda08 100644 --- a/Include/internal/pycore_runtime_structs.h +++ b/Include/internal/pycore_runtime_structs.h @@ -271,16 +271,6 @@ struct pyruntimestate { struct _types_runtime_state types; struct _Py_time_runtime_state time; -#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE) - // Used in "Python/emscripten_trampoline.c" to choose between wasm-gc - // trampoline and JavaScript trampoline. - PyObject* (*emscripten_trampoline)(int* success, - PyCFunctionWithKeywords func, - PyObject* self, - PyObject* args, - PyObject* kw); -#endif - /* All the objects that are shared by the runtime's interpreters. */ struct _Py_cached_objects cached_objects; struct _Py_static_objects static_objects; diff --git a/Include/internal/pycore_emscripten_trampoline.h b/Include/internal/pycore_wasm_trampoline.h similarity index 56% rename from Include/internal/pycore_emscripten_trampoline.h rename to Include/internal/pycore_wasm_trampoline.h index e37c53a64f4a72..1199bab0aca097 100644 --- a/Include/internal/pycore_emscripten_trampoline.h +++ b/Include/internal/pycore_wasm_trampoline.h @@ -1,7 +1,7 @@ -#ifndef Py_EMSCRIPTEN_TRAMPOLINE_H -#define Py_EMSCRIPTEN_TRAMPOLINE_H +#ifndef Py_WASM_TRAMPOLINE_H +#define Py_WASM_TRAMPOLINE_H -#include "pycore_typedefs.h" // _PyRuntimeState +#include "Python.h" /** * C function call trampolines to mitigate bad function pointer casts. @@ -18,37 +18,44 @@ * with 0/NULL in function pointer cast. Compilers do not show warnings when a * function pointer is explicitly casted to an incompatible type. * - * Bad fpcasts are an issue in WebAssembly. WASM's indirect_call has strict + * Bad fpcasts are an issue in WebAssembly. Wasm's indirect_call has strict * function signature checks. Argument count, types, and return type must match. * * Third party code unintentionally rely on problematic fpcasts. The call - * trampoline mitigates common occurrences of bad fpcasts on Emscripten. + * trampoline mitigates common occurrences of bad fpcasts on Wasm targets. */ -#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE) +#if defined(__wasm__) && defined(PY_CALL_TRAMPOLINE) PyObject* -_PyEM_TrampolineCall(PyCFunctionWithKeywords func, - PyObject* self, - PyObject* args, - PyObject* kw); +_PyWasm_TrampolineCall(PyCFunctionWithKeywords func, + PyObject* self, + PyObject* args, + PyObject* kw); + +int +_PyWasm_TrampolineCallSetter(setter func, + PyObject* self, + PyObject* val, + void* closure); #define _PyCFunction_TrampolineCall(meth, self, args) \ - _PyEM_TrampolineCall(*_PyCFunctionWithKeywords_CAST(meth), (self), (args), NULL) + _PyWasm_TrampolineCall(*_PyCFunctionWithKeywords_CAST(meth), (self), (args), NULL) #define _PyCFunctionWithKeywords_TrampolineCall(meth, self, args, kw) \ - _PyEM_TrampolineCall((meth), (self), (args), (kw)) + _PyWasm_TrampolineCall((meth), (self), (args), (kw)) #define descr_set_trampoline_call(set, obj, value, closure) \ - ((int)_PyEM_TrampolineCall(_PyCFunctionWithKeywords_CAST(set), (obj), \ - (value), (PyObject*)(closure))) + _PyWasm_TrampolineCallSetter((set), (obj), (value), (closure)) #define descr_get_trampoline_call(get, obj, closure) \ - _PyEM_TrampolineCall(_PyCFunctionWithKeywords_CAST(get), (obj), \ - (PyObject*)(closure), NULL) + _PyWasm_TrampolineCall(_PyCFunctionWithKeywords_CAST(get), (obj), \ + (PyObject*)(closure), NULL) + +#else // defined(__wasm__) && defined(PY_CALL_TRAMPOLINE) -#else // defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE) +// Disable trampolines by directly calling the method. #define _PyCFunction_TrampolineCall(meth, self, args) \ (meth)((self), (args)) @@ -62,6 +69,6 @@ _PyEM_TrampolineCall(PyCFunctionWithKeywords func, #define descr_get_trampoline_call(get, obj, closure) \ (get)((obj), (closure)) -#endif // defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE) +#endif // defined(__wasm__) && defined(PY_CALL_TRAMPOLINE) -#endif // ndef Py_EMSCRIPTEN_SIGNAL_H +#endif // ndef Py_WASM_TRAMPOLINE_H diff --git a/Lib/test/test_capi/test_fpcast.py b/Lib/test/test_capi/test_fpcast.py new file mode 100644 index 00000000000000..e687d36d645c66 --- /dev/null +++ b/Lib/test/test_capi/test_fpcast.py @@ -0,0 +1,73 @@ +"""Tests for calling C functions through mis-cast function pointers. + +Extension modules frequently cast C functions with the wrong number of +arguments to PyCFunction, getter, setter or ternaryfunc. Native ABIs tolerate +this; on WebAssembly, call_indirect checks the signature, so CPython routes +these calls through a trampoline (Python/wasm_trampoline.c) that detects the +real signature. Calling each variant must work everywhere. +""" +import unittest +from test.support import import_helper, is_wasm32 + +_testcapi = import_helper.import_module('_testcapi') + + +class FpcastTest(unittest.TestCase): + def check_calls(self, obj, prefix): + for arity in range(4): + with self.subTest(kind="noargs", arity=arity): + self.assertIsNone(getattr(obj, f"{prefix}noargs{arity}")()) + with self.subTest(kind="o", arity=arity): + self.assertIsNone(getattr(obj, f"{prefix}o{arity}")(1)) + with self.subTest(kind="varargs", arity=arity): + self.assertIsNone(getattr(obj, f"{prefix}varargs{arity}")()) + self.assertIsNone(getattr(obj, f"{prefix}varargs{arity}")(1, 2)) + with self.subTest(kind="kwargs", arity=arity): + self.assertIsNone(getattr(obj, f"{prefix}kwargs{arity}")()) + self.assertIsNone(getattr(obj, f"{prefix}kwargs{arity}")(1, x=2)) + + def test_module_functions(self): + self.check_calls(_testcapi, "fpcast_") + + def test_methods(self): + self.check_calls(_testcapi.FpcastTestType(), "") + + def test_tp_call(self): + for arity in range(4): + with self.subTest(arity=arity): + cls = getattr(_testcapi, f"FpcastCallable{arity}") + self.assertIsNone(cls()()) + self.assertIsNone(cls()(1, x=2)) + + def test_getset(self): + t = _testcapi.FpcastTestType() + self.assertIsNone(t.getset0) + self.assertIsNone(t.getset1) + self.assertIsNone(t.getset2) + t.getset1 = 5 + sentinel = object() + t.getset2 = sentinel + self.assertIs(_testcapi.fpcast_last_set_value(), sentinel) + with self.assertRaises(AttributeError): + t.getset0 = 1 + + @unittest.skipUnless(is_wasm32, "requires the wasm call trampoline") + def test_unsupported_signature(self): + # A function with four pointer arguments matches none of the + # signatures the trampoline knows about: it must raise SystemError + # rather than trap. + t = _testcapi.FpcastTestType() + with self.assertRaises(SystemError): + _testcapi.fpcast_noargs4() + with self.assertRaises(SystemError): + t.noargs4() + with self.assertRaises(SystemError): + _testcapi.FpcastCallable4()() + with self.assertRaises(SystemError): + t.getset4 + with self.assertRaises(SystemError): + t.getset4 = 1 + + +if __name__ == "__main__": + unittest.main() diff --git a/Makefile.pre.in b/Makefile.pre.in index 42480f28ad7f31..36515dbd146a1e 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -1439,6 +1439,7 @@ PYTHON_HEADERS= \ $(srcdir)/Include/internal/pycore_uop.h \ $(srcdir)/Include/internal/pycore_uop_ids.h \ $(srcdir)/Include/internal/pycore_uop_metadata.h \ + $(srcdir)/Include/internal/pycore_wasm_trampoline.h \ $(srcdir)/Include/internal/pycore_warnings.h \ $(srcdir)/Include/internal/pycore_weakref.h \ $(DTRACE_HEADERS) \ @@ -3153,13 +3154,6 @@ Python/asm_trampoline_universal2.o: $(srcdir)/Python/asm_trampoline_aarch64.S $( rm -f Python/asm_trampoline_arm64-apple-darwin.o \ Python/asm_trampoline_x86_64-apple-darwin.o -Python/emscripten_trampoline_inner.wasm: $(srcdir)/Python/emscripten_trampoline_inner.c - # emcc has a path that ends with emsdk/upstream/emscripten/emcc, we're looking for emsdk/upstream/bin/clang. - $$(em-config LLVM_ROOT)/clang -o $@ $< -mgc -O2 -Wl,--no-entry -Wl,--import-table -Wl,--import-memory -target wasm32-unknown-unknown -nostdlib - -Python/emscripten_trampoline_wasm.c: Python/emscripten_trampoline_inner.wasm - $(PYTHON_FOR_REGEN) $(srcdir)/Platforms/emscripten/prepare_external_wasm.py $< $@ getWasmTrampolineModule - JIT_SHIM_BUILD_OBJS= @JIT_SHIM_BUILD_O@ JIT_UNWIND_INFO_H= $(if $(JIT_OBJS),jit_unwind_info.h $(patsubst jit_stencils-%.h,jit_unwind_info-%.h,@JIT_STENCILS_H@)) JIT_BUILD_TARGETS= jit_stencils.h @JIT_STENCILS_H@ $(JIT_UNWIND_INFO_H) $(JIT_SHIM_BUILD_OBJS) diff --git a/Misc/NEWS.d/next/Build/2026-10-02-12-18-20.gh-issue-129040.6wJPY6.rst b/Misc/NEWS.d/next/Build/2026-10-02-12-18-20.gh-issue-129040.6wJPY6.rst new file mode 100644 index 00000000000000..72f27069d87936 --- /dev/null +++ b/Misc/NEWS.d/next/Build/2026-10-02-12-18-20.gh-issue-129040.6wJPY6.rst @@ -0,0 +1,11 @@ +Added support for function call adaptor trampolines for wasi to prevent +traps when C handlers take the wrong number of arguments. Dropped Emscripten +function call adaptors support for JS runtimes that don't support wasm-gc. + +#.. section: Windows #.. section: macOS #.. section: IDLE #.. section: +Tools/Demos #.. section: C API + +# Write your Misc/NEWS.d entry below. It should be a simple ReST paragraph. +# Don't start with "- Issue #: " or "- gh-issue-: " or that sort of +stuff. +########################################################################### diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index dd9f38e59f7a3f..d8cd8de10dfe08 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -173,7 +173,7 @@ @MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c @MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c @MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c _testinternalcapi/interpreter.c _testinternalcapi/tokenizer.c _testinternalcapi/tuple.c _testinternalcapi/typecache.c -@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.c _testcapi/weakref.c _testcapi/marshal.c +@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.c _testcapi/weakref.c _testcapi/marshal.c _testcapi/fpcast.c @MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/capsule.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/slice.c _testlimitedcapi/slots.c _testlimitedcapi/sys.c _testlimitedcapi/threadstate.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c _testlimitedcapi/weakref.c _testlimitedcapi/run.c _testlimitedcapi/type.c _testlimitedcapi/hash.c _testlimitedcapi/build.c @MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c @MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c diff --git a/Modules/_testcapi/fpcast.c b/Modules/_testcapi/fpcast.c new file mode 100644 index 00000000000000..0563b310d126cb --- /dev/null +++ b/Modules/_testcapi/fpcast.c @@ -0,0 +1,171 @@ +/* + * Tests for the WebAssembly function pointer cast trampoline + * (Python/wasm_trampoline.c). + * + * Third party extensions frequently cast functions with the "wrong" number of + * arguments to PyCFunction, getter, setter or ternaryfunc. On native targets + * this works by accident; on WebAssembly, call_indirect checks the signature + * and traps, so CPython routes these calls through a trampoline that detects + * the real signature. + */ + +#include "parts.h" + +static PyObject * +zero(void) +{ + Py_RETURN_NONE; +} + +static PyObject * +one(PyObject *self) +{ + Py_RETURN_NONE; +} + +static PyObject * +two(PyObject *self, PyObject *args) +{ + Py_RETURN_NONE; +} + +static PyObject * +three(PyObject *self, PyObject *args, PyObject *kwargs) +{ + Py_RETURN_NONE; +} + +/* Four pointer arguments: no trampoline signature matches, so calling this + * must raise SystemError instead of trapping. */ +static PyObject * +four(PyObject *self, PyObject *a, PyObject *b, PyObject *c) +{ + Py_RETURN_NONE; +} + +static int +set_two(PyObject *self, PyObject *value) +{ + return 0; +} + +/* Record the arguments the setter actually receives so the test can check + * that the trampoline passes them through correctly. */ +static PyObject *last_set_value = NULL; + +static int +set_three(PyObject *self, PyObject *value, void *closure) +{ + Py_XSETREF(last_set_value, Py_XNewRef(value)); + return 0; +} + +static PyObject * +get_last_set_value(PyObject *self, PyObject *Py_UNUSED(args)) +{ + if (last_set_value == NULL) { + Py_RETURN_NONE; + } + return Py_NewRef(last_set_value); +} + +/* The module-level and type-level tables must be separate arrays: module + * functions are called via cfunction_vectorcall_* / cfunction_call, while + * methods go through method_vectorcall_* in descrobject.c. */ +#define FPCAST_METHODS(prefix) \ + {prefix "noargs0", _PyCFunction_CAST(zero), METH_NOARGS}, \ + {prefix "noargs1", _PyCFunction_CAST(one), METH_NOARGS}, \ + {prefix "noargs2", _PyCFunction_CAST(two), METH_NOARGS}, \ + {prefix "noargs3", _PyCFunction_CAST(three), METH_NOARGS}, \ + {prefix "noargs4", _PyCFunction_CAST(four), METH_NOARGS}, \ + \ + {prefix "o0", _PyCFunction_CAST(zero), METH_O}, \ + {prefix "o1", _PyCFunction_CAST(one), METH_O}, \ + {prefix "o2", _PyCFunction_CAST(two), METH_O}, \ + {prefix "o3", _PyCFunction_CAST(three), METH_O}, \ + \ + {prefix "varargs0", _PyCFunction_CAST(zero), METH_VARARGS}, \ + {prefix "varargs1", _PyCFunction_CAST(one), METH_VARARGS}, \ + {prefix "varargs2", _PyCFunction_CAST(two), METH_VARARGS}, \ + {prefix "varargs3", _PyCFunction_CAST(three), METH_VARARGS}, \ + \ + {prefix "kwargs0", _PyCFunction_CAST(zero), \ + METH_VARARGS | METH_KEYWORDS}, \ + {prefix "kwargs1", _PyCFunction_CAST(one), \ + METH_VARARGS | METH_KEYWORDS}, \ + {prefix "kwargs2", _PyCFunction_CAST(two), \ + METH_VARARGS | METH_KEYWORDS}, \ + {prefix "kwargs3", _PyCFunction_CAST(three), \ + METH_VARARGS | METH_KEYWORDS} + +static PyMethodDef test_methods[] = { + FPCAST_METHODS("fpcast_"), + {"fpcast_last_set_value", get_last_set_value, METH_NOARGS}, + {NULL}, +}; + +static PyMethodDef type_methods[] = { + FPCAST_METHODS(""), + {NULL}, +}; + +static PyGetSetDef type_getset[] = { + {"getset0", .get = _Py_FUNC_CAST(getter, zero)}, + {"getset1", .get = _Py_FUNC_CAST(getter, one), + .set = _Py_FUNC_CAST(setter, set_two)}, + {"getset2", .get = _Py_FUNC_CAST(getter, two), + .set = _Py_FUNC_CAST(setter, set_three)}, + {"getset4", .get = _Py_FUNC_CAST(getter, four), + .set = _Py_FUNC_CAST(setter, four)}, + {NULL}, +}; + +static PyTypeObject FpcastTestType = { + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "_testcapi.FpcastTestType", + .tp_basicsize = sizeof(PyObject), + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_methods = type_methods, + .tp_getset = type_getset, + .tp_new = PyType_GenericNew, +}; + +#define CALLABLE_TYPE(N, FUNC) \ + static PyTypeObject FpcastCallable##N = { \ + PyVarObject_HEAD_INIT(NULL, 0) \ + .tp_name = "_testcapi.FpcastCallable" #N, \ + .tp_basicsize = sizeof(PyObject), \ + .tp_flags = Py_TPFLAGS_DEFAULT, \ + .tp_call = _Py_FUNC_CAST(ternaryfunc, FUNC), \ + .tp_new = PyType_GenericNew, \ + }; + +CALLABLE_TYPE(0, zero) +CALLABLE_TYPE(1, one) +CALLABLE_TYPE(2, two) +CALLABLE_TYPE(3, three) +CALLABLE_TYPE(4, four) + +#undef CALLABLE_TYPE + +int +_PyTestCapi_Init_Fpcast(PyObject *mod) +{ + if (PyModule_AddFunctions(mod, test_methods) < 0) { + return -1; + } + PyTypeObject *types[] = { + &FpcastTestType, + &FpcastCallable0, + &FpcastCallable1, + &FpcastCallable2, + &FpcastCallable3, + &FpcastCallable4, + }; + for (size_t i = 0; i < Py_ARRAY_LENGTH(types); i++) { + if (PyModule_AddType(mod, types[i]) < 0) { + return -1; + } + } + return 0; +} diff --git a/Modules/_testcapi/parts.h b/Modules/_testcapi/parts.h index 1ae3f0773e42f8..e2787e7c93aef8 100644 --- a/Modules/_testcapi/parts.h +++ b/Modules/_testcapi/parts.h @@ -69,5 +69,6 @@ int _PyTestCapi_Init_Function(PyObject *mod); int _PyTestCapi_Init_Module(PyObject *mod); int _PyTestCapi_Init_Weakref(PyObject *mod); int _PyTestCapi_Init_Marshal(PyObject *mod); +int _PyTestCapi_Init_Fpcast(PyObject *mod); #endif // Py_TESTCAPI_PARTS_H diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 19c02e6774e3d4..033ee3234cf604 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -3832,6 +3832,9 @@ _testcapi_exec(PyObject *m) if (_PyTestCapi_Init_Marshal(m) < 0) { return -1; } + if (_PyTestCapi_Init_Fpcast(m) < 0) { + return -1; + } return 0; } diff --git a/Objects/descrobject.c b/Objects/descrobject.c index 63023887667a50..c0af1ed8cad830 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -4,7 +4,7 @@ #include "pycore_abstract.h" // _PyObject_RealIsSubclass() #include "pycore_call.h" // _PyStack_AsDict() #include "pycore_ceval.h" // _Py_EnterRecursiveCallTstate() -#include "pycore_emscripten_trampoline.h" // descr_set_trampoline_call(), descr_get_trampoline_call() +#include "pycore_wasm_trampoline.h" // descr_set_trampoline_call(), descr_get_trampoline_call() #include "pycore_descrobject.h" // _PyMethodWrapper_Type #include "pycore_modsupport.h" // _PyArg_UnpackStack() #include "pycore_object.h" // _PyObject_GC_UNTRACK() diff --git a/PCbuild/_testcapi.vcxproj b/PCbuild/_testcapi.vcxproj index d856b70bbdd579..9b4473b877daf4 100644 --- a/PCbuild/_testcapi.vcxproj +++ b/PCbuild/_testcapi.vcxproj @@ -135,6 +135,7 @@ + diff --git a/PCbuild/_testcapi.vcxproj.filters b/PCbuild/_testcapi.vcxproj.filters index 554e5f3075f7eb..238819a1d3646f 100644 --- a/PCbuild/_testcapi.vcxproj.filters +++ b/PCbuild/_testcapi.vcxproj.filters @@ -138,6 +138,9 @@ Source Files + + Source Files + diff --git a/Platforms/WASI/_build.py b/Platforms/WASI/_build.py index fff7e3f9f8cdad..5a697bc4e9a405 100644 --- a/Platforms/WASI/_build.py +++ b/Platforms/WASI/_build.py @@ -255,6 +255,7 @@ def configure_wasi_python(context, working_dir): f"--host={context.host_triple}", f"--build={context.build_python_path.name}", f"--with-build-python={build_python}", + "CFLAGS=-DPY_CALL_TRAMPOLINE", ] if context.is_debug: configure.append("--with-pydebug") diff --git a/Python/emscripten_trampoline.c b/Python/emscripten_trampoline.c deleted file mode 100644 index 547761027a3fb3..00000000000000 --- a/Python/emscripten_trampoline.c +++ /dev/null @@ -1,136 +0,0 @@ -#if defined(PY_CALL_TRAMPOLINE) - -#include // EM_JS, EM_JS_DEPS -#include -#include "pycore_runtime.h" // _PyRuntime - -// We use the _PyRuntime.emscripten_trampoline field to store a function pointer -// for a wasm-gc based trampoline if it works. Otherwise fall back to JS -// trampoline. The JS trampoline breaks stack switching but every runtime that -// supports stack switching also supports wasm-gc. -// -// We'd like to make the trampoline call into a direct call but currently we -// need to import the wasmTable to compile trampolineModule. emcc >= 4.0.19 -// defines the table in WebAssembly and exports it so we won't have access to it -// until after the main module is compiled. -// -// To fix this, one natural solution would be to pass a funcref to the -// trampoline instead of a table index. Several PRs would be needed to fix -// things in llvm and emscripten in order to make this possible. -// -// The performance costs of an extra call_indirect aren't that large anyways. -// The JIT should notice that the target is always the same and turn into a -// check -// -// if (call_target != expected) deoptimize; -// direct_call(call_target, args); - -// Offset of emscripten_trampoline in _PyRuntimeState. There's a couple of -// alternatives: -// -// 1. Just make emscripten_trampoline a real C global variable instead of a -// field of _PyRuntimeState. This would violate our rule against mutable -// globals. -// -// 2. #define a preprocessor constant equal to a hard coded number and make a -// _Static_assert(offsetof(_PyRuntimeState, emscripten_trampoline) == OURCONSTANT) -// This has the disadvantage that we have to update the hard coded constant -// when _PyRuntimeState changes -// -// So putting the mutable constant in _PyRuntime and using a immutable global to -// record the offset so we can access it from JS is probably the best way. -EMSCRIPTEN_KEEPALIVE const int _PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET = offsetof(_PyRuntimeState, emscripten_trampoline); - -typedef PyObject* (*TrampolineFunc)(int* success, - PyCFunctionWithKeywords func, - PyObject* self, - PyObject* args, - PyObject* kw); - -// Lets JS reach _PyRuntime without it being in -sEXPORTED_FUNCTIONS. -EMSCRIPTEN_KEEPALIVE _PyRuntimeState *const _PyEM_runtime = &_PyRuntime; - -// Its table slot is taken over by the wasm-gc trampoline, so the table -// never grows. -static PyObject* -trampoline_placeholder(int* success, PyCFunctionWithKeywords func, - PyObject* self, PyObject* args, PyObject* kw) -{ - Py_FatalError("Emscripten trampoline slot was not set up"); -} -EMSCRIPTEN_KEEPALIVE const TrampolineFunc _PyEM_trampoline_slot = trampoline_placeholder; - -/** - * Backwards compatible trampoline works with all JS runtimes - */ -EM_JS(PyObject*, _PyEM_TrampolineCall_JS, (PyCFunctionWithKeywords func, PyObject *arg1, PyObject *arg2, PyObject *arg3), { - return wasmTable.get(func)(arg1, arg2, arg3); -} -// Try to compile wasm-gc trampoline if possible. -function getPyEMTrampolinePtr() { - // Starting with iOS 18.3.1, WebKit on iOS has an issue with the garbage - // collector that breaks the call trampoline. See #130418 and - // https://bugs.webkit.org/show_bug.cgi?id=293113 for details. - let isIOS = globalThis.navigator && ( - /iPad|iPhone|iPod/.test(navigator.userAgent) || - // Starting with iPadOS 13, iPads might send a platform string that looks like a desktop Mac. - // To differentiate, we check if the platform is 'MacIntel' (common for Macs and newer iPads) - // AND if the device has multi-touch capabilities (navigator.maxTouchPoints > 1) - (navigator.platform === 'MacIntel' && typeof navigator.maxTouchPoints !== 'undefined' && navigator.maxTouchPoints > 1) - ); - if (isIOS) { - return 0; - } - let trampolineModule; - try { - trampolineModule = getWasmTrampolineModule(); - } catch (e) { - // Compilation error due to missing wasm-gc support, fall back to JS - // trampoline - return 0; - } - const trampolineInstance = new WebAssembly.Instance(trampolineModule, { - env: { __indirect_function_table: wasmTable, memory: wasmMemory }, - }); - const slot = HEAPU32[__PyEM_trampoline_slot / 4]; - wasmTable.set(slot, trampolineInstance.exports.trampoline_call); - return slot; -} -// We have to be careful to work correctly with memory snapshots -- the value of -// _PyRuntimeState.emscripten_trampoline needs to reflect whether wasm-gc is -// available in the current runtime, not in the runtime the snapshot was taken -// in. This writes the appropriate value to -// _PyRuntimeState.emscripten_trampoline from JS startup code that runs every -// time, whether we are restoring a snapshot or not. -addOnPreRun(function setEmscriptenTrampoline() { - const ptr = getPyEMTrampolinePtr(); - const offset = HEAP32[__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET / 4]; - HEAP32[(HEAPU32[__PyEM_runtime / 4] + offset) / 4] = ptr; -}); -); - -EM_JS_DEPS(_PyEM_TrampolineCall, - "$wasmTable,$wasmMemory,$addOnPreRun"); - -PyObject* -_PyEM_TrampolineCall(PyCFunctionWithKeywords func, - PyObject* self, - PyObject* args, - PyObject* kw) -{ - TrampolineFunc trampoline = _PyRuntime.emscripten_trampoline; - if (trampoline == 0) { - return _PyEM_TrampolineCall_JS(func, self, args, kw); - } - int success = 1; - PyObject *result = trampoline(&success, func, self, args, kw); - if (!success) { - PyErr_SetString(PyExc_SystemError, "Handler takes too many arguments"); - } - return result; -} - -#else -// This is exported so we need to define it even when it isn't used -__attribute__((used)) const int _PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET = 0; -#endif diff --git a/Python/emscripten_trampoline_inner.c b/Python/emscripten_trampoline_inner.c deleted file mode 100644 index a2bad4857ed089..00000000000000 --- a/Python/emscripten_trampoline_inner.c +++ /dev/null @@ -1,38 +0,0 @@ -// This file must be compiled with -mgc to enable the extra wasm-gc -// instructions. It has to be compiled separately because not enough JS runtimes -// support wasm-gc yet. If the JS runtime does not support wasm-gc (or has buggy -// support like iOS), we will use the JS trampoline fallback. - -// We can't import Python.h here because it is compiled/linked with -nostdlib. -// We don't need to know what's inside PyObject* anyways. We could just call it -// void* everywhere. There are two reasons to do this: -// 1. to improve readability -// 2. eventually when we are comfortable requiring wasm-gc, we can merge this -// into emscripten_trampoline.c without worrying about it. -typedef void PyObject; - -typedef PyObject* (*three_arg)(PyObject*, PyObject*, PyObject*); -typedef PyObject* (*two_arg)(PyObject*, PyObject*); -typedef PyObject* (*one_arg)(PyObject*); -typedef PyObject* (*zero_arg)(void); - -#define TRY_RETURN_CALL(ty, args...) \ - if (__builtin_wasm_test_function_pointer_signature((ty)func)) { \ - return ((ty)func)(args); \ - } - -__attribute__((export_name("trampoline_call"))) PyObject* -trampoline_call(int* success, - void* func, - PyObject* self, - PyObject* args, - PyObject* kw) -{ - *success = 1; - TRY_RETURN_CALL(three_arg, self, args, kw); - TRY_RETURN_CALL(two_arg, self, args); - TRY_RETURN_CALL(one_arg, self); - TRY_RETURN_CALL(zero_arg); - *success = 0; - return 0; -} diff --git a/Python/wasm_trampoline.c b/Python/wasm_trampoline.c new file mode 100644 index 00000000000000..e449ff72343e5c --- /dev/null +++ b/Python/wasm_trampoline.c @@ -0,0 +1,48 @@ +#if defined(__wasm__) && defined(PY_CALL_TRAMPOLINE) + +#include + +typedef PyObject* (*three_arg)(PyObject*, PyObject*, PyObject*); +typedef PyObject* (*two_arg)(PyObject*, PyObject*); +typedef PyObject* (*one_arg)(PyObject*); +typedef PyObject* (*zero_arg)(void); + +#define TRY_RETURN_CALL(ty, args...) \ + if (__builtin_wasm_test_function_pointer_signature((ty)func)) { \ + return ((ty)func)(args); \ + } + +PyObject* +_PyWasm_TrampolineCall(PyCFunctionWithKeywords func, + PyObject* self, + PyObject* args, + PyObject* kw) +{ + TRY_RETURN_CALL(three_arg, self, args, kw); + TRY_RETURN_CALL(two_arg, self, args); + TRY_RETURN_CALL(one_arg, self); + TRY_RETURN_CALL(zero_arg); + PyErr_SetString(PyExc_SystemError, "Handler has incorrect signature"); + return NULL; +} + +typedef int (*setter_three_arg)(PyObject*, PyObject*, void*); +typedef int (*setter_two_arg)(PyObject*, PyObject*); +typedef int (*setter_one_arg)(PyObject*); +typedef int (*setter_zero_arg)(void); + +int +_PyWasm_TrampolineCallSetter(setter func, + PyObject* self, + PyObject* val, + void* closure) +{ + TRY_RETURN_CALL(setter_three_arg, self, val, closure); + TRY_RETURN_CALL(setter_two_arg, self, val); + TRY_RETURN_CALL(setter_one_arg, self); + TRY_RETURN_CALL(setter_zero_arg); + PyErr_SetString(PyExc_SystemError, "Handler has incorrect signature"); + return -1; +} + +#endif // defined(__wasm__) && defined(PY_CALL_TRAMPOLINE) diff --git a/Tools/c-analyzer/cpython/_parser.py b/Tools/c-analyzer/cpython/_parser.py index 3c715f91cede10..caac194d3b7efb 100644 --- a/Tools/c-analyzer/cpython/_parser.py +++ b/Tools/c-analyzer/cpython/_parser.py @@ -63,7 +63,6 @@ def format_tsv_lines(lines): 'Python/dynload_hpux.c', # dl.h 'Python/emscripten_signal.c', 'Python/emscripten_syscalls.c', - 'Python/emscripten_trampoline_inner.c', 'Python/thread_pthread.h', 'Python/thread_pthread_stubs.h', diff --git a/configure b/configure index 7d1003a0e0e985..4e763f58ae581f 100755 --- a/configure +++ b/configure @@ -9970,7 +9970,7 @@ fi as_fn_append LINKFORSHARED " -sFORCE_FILESYSTEM -lidbfs.js -lnodefs.js -lproxyfs.js -lworkerfs.js" as_fn_append LINKFORSHARED " -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32,TTY,ERRNO_CODES" - as_fn_append LINKFORSHARED " -sEXPORTED_FUNCTIONS=_main,_Py_Version,_PyGILState_GetThisThreadState,__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET" + as_fn_append LINKFORSHARED " -sEXPORTED_FUNCTIONS=_main,_Py_Version,_PyGILState_GetThisThreadState" as_fn_append LINKFORSHARED " -sSTACK_SIZE=5MB" as_fn_append LINKFORSHARED " -sTEXTDECODER=2" @@ -20303,14 +20303,21 @@ PLATFORM_OBJS= case $ac_sys_system in #( Emscripten) : - as_fn_append PLATFORM_OBJS ' Python/emscripten_signal.o Python/emscripten_trampoline.o Python/emscripten_trampoline_wasm.o' + as_fn_append PLATFORM_OBJS ' Python/emscripten_signal.o Python/wasm_trampoline.o' + as_fn_append CFLAGS_NODIST " -mgc" + if test "x$enable_emscripten_syscalls" = xyes then : as_fn_append PLATFORM_OBJS ' Python/emscripten_syscalls.o' fi - as_fn_append PLATFORM_HEADERS ' $(srcdir)/Include/internal/pycore_emscripten_signal.h $(srcdir)/Include/internal/pycore_emscripten_trampoline.h' + as_fn_append PLATFORM_HEADERS ' $(srcdir)/Include/internal/pycore_emscripten_signal.h' + ;; #( + WASI) : + + as_fn_append PLATFORM_OBJS ' Python/wasm_trampoline.o' + as_fn_append CFLAGS_NODIST " -mgc" ;; #( *) : ;; diff --git a/configure.ac b/configure.ac index e1d55a7a3cef90..026cf15d9d8e62 100644 --- a/configure.ac +++ b/configure.ac @@ -2443,7 +2443,7 @@ AS_CASE([$ac_sys_system], dnl Include file system support AS_VAR_APPEND([LINKFORSHARED], [" -sFORCE_FILESYSTEM -lidbfs.js -lnodefs.js -lproxyfs.js -lworkerfs.js"]) AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32,TTY,ERRNO_CODES"]) - AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_FUNCTIONS=_main,_Py_Version,_PyGILState_GetThisThreadState,__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET"]) + AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_FUNCTIONS=_main,_Py_Version,_PyGILState_GetThisThreadState"]) AS_VAR_APPEND([LINKFORSHARED], [" -sSTACK_SIZE=5MB"]) dnl Avoid bugs in JS fallback string decoding path AS_VAR_APPEND([LINKFORSHARED], [" -sTEXTDECODER=2"]) @@ -5443,11 +5443,17 @@ PLATFORM_OBJS= AS_CASE([$ac_sys_system], [Emscripten], [ - AS_VAR_APPEND([PLATFORM_OBJS], [' Python/emscripten_signal.o Python/emscripten_trampoline.o Python/emscripten_trampoline_wasm.o']) + AS_VAR_APPEND([PLATFORM_OBJS], [' Python/emscripten_signal.o Python/wasm_trampoline.o']) + AS_VAR_APPEND([CFLAGS_NODIST], [" -mgc"]) + AS_VAR_IF([enable_emscripten_syscalls], [yes], [ AS_VAR_APPEND([PLATFORM_OBJS], [' Python/emscripten_syscalls.o']) ]) - AS_VAR_APPEND([PLATFORM_HEADERS], [' $(srcdir)/Include/internal/pycore_emscripten_signal.h $(srcdir)/Include/internal/pycore_emscripten_trampoline.h']) + AS_VAR_APPEND([PLATFORM_HEADERS], [' $(srcdir)/Include/internal/pycore_emscripten_signal.h']) + ], + [WASI], [ + AS_VAR_APPEND([PLATFORM_OBJS], [' Python/wasm_trampoline.o']) + AS_VAR_APPEND([CFLAGS_NODIST], [" -mgc"]) ], ) AC_SUBST([PLATFORM_HEADERS]) From 6282bf36652994680d44a1bcdbf4efb165152449 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Fri, 2 Oct 2026 12:24:40 -0700 Subject: [PATCH 2/6] Fix news entry --- .../Build/2026-10-02-12-18-20.gh-issue-129040.6wJPY6.rst | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Misc/NEWS.d/next/Build/2026-10-02-12-18-20.gh-issue-129040.6wJPY6.rst b/Misc/NEWS.d/next/Build/2026-10-02-12-18-20.gh-issue-129040.6wJPY6.rst index 72f27069d87936..50f432d71e22f3 100644 --- a/Misc/NEWS.d/next/Build/2026-10-02-12-18-20.gh-issue-129040.6wJPY6.rst +++ b/Misc/NEWS.d/next/Build/2026-10-02-12-18-20.gh-issue-129040.6wJPY6.rst @@ -1,11 +1,3 @@ Added support for function call adaptor trampolines for wasi to prevent traps when C handlers take the wrong number of arguments. Dropped Emscripten function call adaptors support for JS runtimes that don't support wasm-gc. - -#.. section: Windows #.. section: macOS #.. section: IDLE #.. section: -Tools/Demos #.. section: C API - -# Write your Misc/NEWS.d entry below. It should be a simple ReST paragraph. -# Don't start with "- Issue #: " or "- gh-issue-: " or that sort of -stuff. -########################################################################### From cf80a1afa4923904274170d12b6d37e9a02881d0 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Fri, 2 Oct 2026 12:25:55 -0700 Subject: [PATCH 3/6] Remove no-longer-used prepare_external_wasm.py build helper --- Platforms/emscripten/prepare_external_wasm.py | 53 ------------------- 1 file changed, 53 deletions(-) delete mode 100644 Platforms/emscripten/prepare_external_wasm.py diff --git a/Platforms/emscripten/prepare_external_wasm.py b/Platforms/emscripten/prepare_external_wasm.py deleted file mode 100644 index 1b0a9de4b1fe8d..00000000000000 --- a/Platforms/emscripten/prepare_external_wasm.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import sys -from pathlib import Path - -JS_TEMPLATE = """ -#include "emscripten.h" - -EM_JS(void, {function_name}, (void), {{ - return new WebAssembly.Module(hexStringToUTF8Array("{hex_string}")); -}} -function hexStringToUTF8Array(hex) {{ - const bytes = []; - for (let i = 0; i < hex.length; i += 2) {{ - bytes.push(parseInt(hex.substr(i, 2), 16)); - }} - return new Uint8Array(bytes); -}}); -""" - - -def prepare_wasm(input_file, output_file, function_name): - # Read the compiled WASM as binary and convert to hex - wasm_bytes = Path(input_file).read_bytes() - - hex_string = "".join(f"{byte:02x}" for byte in wasm_bytes) - - # Generate JavaScript module - js_content = JS_TEMPLATE.format( - function_name=function_name, hex_string=hex_string - ) - Path(output_file).write_text(js_content) - - print(f"Successfully compiled {input_file} and generated {output_file}") - return 0 - - -def main(): - parser = argparse.ArgumentParser( - description="Compile WebAssembly text files using wasm-as" - ) - parser.add_argument("input_file", help="Input .wat file to compile") - parser.add_argument("output_file", help="Output file name") - parser.add_argument("function_name", help="Name of the export function") - - args = parser.parse_args() - - return prepare_wasm(args.input_file, args.output_file, args.function_name) - - -if __name__ == "__main__": - sys.exit(main()) From 229729ee3a133b3191c6755278c9968a3b3c046b Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Fri, 2 Oct 2026 12:30:44 -0700 Subject: [PATCH 4/6] tidy up --- Modules/_testcapi/fpcast.c | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/Modules/_testcapi/fpcast.c b/Modules/_testcapi/fpcast.c index 0563b310d126cb..ed6e2d3c5c5e6d 100644 --- a/Modules/_testcapi/fpcast.c +++ b/Modules/_testcapi/fpcast.c @@ -1,10 +1,9 @@ /* - * Tests for the WebAssembly function pointer cast trampoline - * (Python/wasm_trampoline.c). + * Tests for Python/wasm_trampoline.c * * Third party extensions frequently cast functions with the "wrong" number of * arguments to PyCFunction, getter, setter or ternaryfunc. On native targets - * this works by accident; on WebAssembly, call_indirect checks the signature + * this works by accident. On WebAssembly, call_indirect checks the signature * and traps, so CPython routes these calls through a trampoline that detects * the real signature. */ @@ -35,8 +34,7 @@ three(PyObject *self, PyObject *args, PyObject *kwargs) Py_RETURN_NONE; } -/* Four pointer arguments: no trampoline signature matches, so calling this - * must raise SystemError instead of trapping. */ +// Using this as a handler should raise a SystemError. static PyObject * four(PyObject *self, PyObject *a, PyObject *b, PyObject *c) { @@ -49,8 +47,6 @@ set_two(PyObject *self, PyObject *value) return 0; } -/* Record the arguments the setter actually receives so the test can check - * that the trampoline passes them through correctly. */ static PyObject *last_set_value = NULL; static int @@ -69,9 +65,6 @@ get_last_set_value(PyObject *self, PyObject *Py_UNUSED(args)) return Py_NewRef(last_set_value); } -/* The module-level and type-level tables must be separate arrays: module - * functions are called via cfunction_vectorcall_* / cfunction_call, while - * methods go through method_vectorcall_* in descrobject.c. */ #define FPCAST_METHODS(prefix) \ {prefix "noargs0", _PyCFunction_CAST(zero), METH_NOARGS}, \ {prefix "noargs1", _PyCFunction_CAST(one), METH_NOARGS}, \ From f92edfeba2be6a240666d281c22f5154c23328f4 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Fri, 2 Oct 2026 13:05:44 -0700 Subject: [PATCH 5/6] Ignore fpcast.c global variables in c-analyzer --- Tools/c-analyzer/cpython/ignored.tsv | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv index 2ad801c671855c..615de640d6d850 100644 --- a/Tools/c-analyzer/cpython/ignored.tsv +++ b/Tools/c-analyzer/cpython/ignored.tsv @@ -464,6 +464,13 @@ Modules/_testcapi/datetime.c - test_run_counter - Modules/_testcapi/docstring.c - DocStringNoSignatureTest - Modules/_testcapi/docstring.c - DocStringUnrepresentableSignatureTest - Modules/_testcapi/exceptions.c - PyRecursingInfinitelyError_Type - +Modules/_testcapi/fpcast.c - last_set_value - +Modules/_testcapi/fpcast.c - FpcastTestType - +Modules/_testcapi/fpcast.c - FpcastCallable0 - +Modules/_testcapi/fpcast.c - FpcastCallable1 - +Modules/_testcapi/fpcast.c - FpcastCallable2 - +Modules/_testcapi/fpcast.c - FpcastCallable3 - +Modules/_testcapi/fpcast.c - FpcastCallable4 - Modules/_testcapi/heaptype.c - _testcapimodule - Modules/_testcapi/mem.c - FmData - Modules/_testcapi/mem.c - FmHook - From 284b3769e46c5cecafd5019425ac2420a0f5655c Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Fri, 2 Oct 2026 13:09:51 -0700 Subject: [PATCH 6/6] Skip fpcast test in ubsan --- Lib/test/test_capi/test_fpcast.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_capi/test_fpcast.py b/Lib/test/test_capi/test_fpcast.py index e687d36d645c66..ae19ac11f6a191 100644 --- a/Lib/test/test_capi/test_fpcast.py +++ b/Lib/test/test_capi/test_fpcast.py @@ -7,11 +7,15 @@ real signature. Calling each variant must work everywhere. """ import unittest -from test.support import import_helper, is_wasm32 +from test.support import check_sanitizer, import_helper, is_wasm32 _testcapi = import_helper.import_module('_testcapi') +# Native ABIs tolerate these calls but they are implementation-defined behavior. +# -fsanitize=undefined correctly trips on them. +@unittest.skipIf(check_sanitizer(ub=True), + "calls through mis-cast function pointers are UB natively") class FpcastTest(unittest.TestCase): def check_calls(self, obj, prefix): for arity in range(4):