Skip to content

Commit 3e8a9bd

Browse files
committed
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.
1 parent 00a1c3a commit 3e8a9bd

20 files changed

Lines changed: 361 additions & 241 deletions

‎Include/internal/pycore_object.h‎

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ extern "C" {
88
# error "this header requires Py_BUILD_CORE define"
99
#endif
1010

11-
#include "pycore_emscripten_trampoline.h" // _PyCFunction_TrampolineCall()
11+
#include "pycore_wasm_trampoline.h" // _PyCFunction_TrampolineCall()
1212
#include "pycore_gc.h" // _PyObject_GC_TRACK()
1313
#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_PTR_ACQUIRE()
1414
#include "pycore_pystate.h" // _PyInterpreterState_GET()
@@ -977,27 +977,6 @@ extern PyObject* _PyObject_NextNotImplemented(PyObject *);
977977
// Export for '_datetime' shared extension
978978
PyAPI_FUNC(PyObject*) _PyObject_GetState(PyObject *);
979979

980-
/* C function call trampolines to mitigate bad function pointer casts.
981-
*
982-
* Typical native ABIs ignore additional arguments or fill in missing
983-
* values with 0/NULL in function pointer cast. Compilers do not show
984-
* warnings when a function pointer is explicitly casted to an
985-
* incompatible type.
986-
*
987-
* Bad fpcasts are an issue in WebAssembly. WASM's indirect_call has strict
988-
* function signature checks. Argument count, types, and return type must
989-
* match.
990-
*
991-
* Third party code unintentionally rely on problematic fpcasts. The call
992-
* trampoline mitigates common occurrences of bad fpcasts on Emscripten.
993-
*/
994-
#if !(defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE))
995-
#define _PyCFunction_TrampolineCall(meth, self, args) \
996-
(meth)((self), (args))
997-
#define _PyCFunctionWithKeywords_TrampolineCall(meth, self, args, kw) \
998-
(meth)((self), (args), (kw))
999-
#endif // __EMSCRIPTEN__ && PY_CALL_TRAMPOLINE
1000-
1001980
// Export these 2 symbols for '_pickle' shared extension
1002981
PyAPI_DATA(PyTypeObject) _PyNone_Type;
1003982
PyAPI_DATA(PyTypeObject) _PyNotImplemented_Type;

‎Include/internal/pycore_runtime_structs.h‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -271,16 +271,6 @@ struct pyruntimestate {
271271
struct _types_runtime_state types;
272272
struct _Py_time_runtime_state time;
273273

274-
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
275-
// Used in "Python/emscripten_trampoline.c" to choose between wasm-gc
276-
// trampoline and JavaScript trampoline.
277-
PyObject* (*emscripten_trampoline)(int* success,
278-
PyCFunctionWithKeywords func,
279-
PyObject* self,
280-
PyObject* args,
281-
PyObject* kw);
282-
#endif
283-
284274
/* All the objects that are shared by the runtime's interpreters. */
285275
struct _Py_cached_objects cached_objects;
286276
struct _Py_static_objects static_objects;
Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
#ifndef Py_EMSCRIPTEN_TRAMPOLINE_H
2-
#define Py_EMSCRIPTEN_TRAMPOLINE_H
1+
#ifndef Py_WASM_TRAMPOLINE_H
2+
#define Py_WASM_TRAMPOLINE_H
33

4-
#include "pycore_typedefs.h" // _PyRuntimeState
4+
#include "Python.h"
55

66
/**
77
* C function call trampolines to mitigate bad function pointer casts.
@@ -18,37 +18,44 @@
1818
* with 0/NULL in function pointer cast. Compilers do not show warnings when a
1919
* function pointer is explicitly casted to an incompatible type.
2020
*
21-
* Bad fpcasts are an issue in WebAssembly. WASM's indirect_call has strict
21+
* Bad fpcasts are an issue in WebAssembly. Wasm's indirect_call has strict
2222
* function signature checks. Argument count, types, and return type must match.
2323
*
2424
* Third party code unintentionally rely on problematic fpcasts. The call
25-
* trampoline mitigates common occurrences of bad fpcasts on Emscripten.
25+
* trampoline mitigates common occurrences of bad fpcasts on Wasm targets.
2626
*/
2727

28-
#if defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
28+
#if defined(__wasm__) && defined(PY_CALL_TRAMPOLINE)
2929

3030
PyObject*
31-
_PyEM_TrampolineCall(PyCFunctionWithKeywords func,
32-
PyObject* self,
33-
PyObject* args,
34-
PyObject* kw);
31+
_PyWasm_TrampolineCall(PyCFunctionWithKeywords func,
32+
PyObject* self,
33+
PyObject* args,
34+
PyObject* kw);
35+
36+
int
37+
_PyWasm_TrampolineCallSetter(setter func,
38+
PyObject* self,
39+
PyObject* val,
40+
void* closure);
3541

3642
#define _PyCFunction_TrampolineCall(meth, self, args) \
37-
_PyEM_TrampolineCall(*_PyCFunctionWithKeywords_CAST(meth), (self), (args), NULL)
43+
_PyWasm_TrampolineCall(*_PyCFunctionWithKeywords_CAST(meth), (self), (args), NULL)
3844

3945
#define _PyCFunctionWithKeywords_TrampolineCall(meth, self, args, kw) \
40-
_PyEM_TrampolineCall((meth), (self), (args), (kw))
46+
_PyWasm_TrampolineCall((meth), (self), (args), (kw))
4147

4248
#define descr_set_trampoline_call(set, obj, value, closure) \
43-
((int)_PyEM_TrampolineCall(_PyCFunctionWithKeywords_CAST(set), (obj), \
44-
(value), (PyObject*)(closure)))
49+
_PyWasm_TrampolineCallSetter((set), (obj), (value), (closure))
4550

4651
#define descr_get_trampoline_call(get, obj, closure) \
47-
_PyEM_TrampolineCall(_PyCFunctionWithKeywords_CAST(get), (obj), \
48-
(PyObject*)(closure), NULL)
52+
_PyWasm_TrampolineCall(_PyCFunctionWithKeywords_CAST(get), (obj), \
53+
(PyObject*)(closure), NULL)
54+
4955

56+
#else // defined(__wasm__) && defined(PY_CALL_TRAMPOLINE)
5057

51-
#else // defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
58+
// Disable trampolines by directly calling the method.
5259

5360
#define _PyCFunction_TrampolineCall(meth, self, args) \
5461
(meth)((self), (args))
@@ -62,6 +69,6 @@ _PyEM_TrampolineCall(PyCFunctionWithKeywords func,
6269
#define descr_get_trampoline_call(get, obj, closure) \
6370
(get)((obj), (closure))
6471

65-
#endif // defined(__EMSCRIPTEN__) && defined(PY_CALL_TRAMPOLINE)
72+
#endif // defined(__wasm__) && defined(PY_CALL_TRAMPOLINE)
6673

67-
#endif // ndef Py_EMSCRIPTEN_SIGNAL_H
74+
#endif // ndef Py_WASM_TRAMPOLINE_H

‎Lib/test/test_capi/test_fpcast.py‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
"""Tests for calling C functions through mis-cast function pointers.
2+
3+
Extension modules frequently cast C functions with the wrong number of
4+
arguments to PyCFunction, getter, setter or ternaryfunc. Native ABIs tolerate
5+
this; on WebAssembly, call_indirect checks the signature, so CPython routes
6+
these calls through a trampoline (Python/wasm_trampoline.c) that detects the
7+
real signature. Calling each variant must work everywhere.
8+
"""
9+
import unittest
10+
from test.support import import_helper, is_wasm32
11+
12+
_testcapi = import_helper.import_module('_testcapi')
13+
14+
15+
class FpcastTest(unittest.TestCase):
16+
def check_calls(self, obj, prefix):
17+
for arity in range(4):
18+
with self.subTest(kind="noargs", arity=arity):
19+
self.assertIsNone(getattr(obj, f"{prefix}noargs{arity}")())
20+
with self.subTest(kind="o", arity=arity):
21+
self.assertIsNone(getattr(obj, f"{prefix}o{arity}")(1))
22+
with self.subTest(kind="varargs", arity=arity):
23+
self.assertIsNone(getattr(obj, f"{prefix}varargs{arity}")())
24+
self.assertIsNone(getattr(obj, f"{prefix}varargs{arity}")(1, 2))
25+
with self.subTest(kind="kwargs", arity=arity):
26+
self.assertIsNone(getattr(obj, f"{prefix}kwargs{arity}")())
27+
self.assertIsNone(getattr(obj, f"{prefix}kwargs{arity}")(1, x=2))
28+
29+
def test_module_functions(self):
30+
self.check_calls(_testcapi, "fpcast_")
31+
32+
def test_methods(self):
33+
self.check_calls(_testcapi.FpcastTestType(), "")
34+
35+
def test_tp_call(self):
36+
for arity in range(4):
37+
with self.subTest(arity=arity):
38+
cls = getattr(_testcapi, f"FpcastCallable{arity}")
39+
self.assertIsNone(cls()())
40+
self.assertIsNone(cls()(1, x=2))
41+
42+
def test_getset(self):
43+
t = _testcapi.FpcastTestType()
44+
self.assertIsNone(t.getset0)
45+
self.assertIsNone(t.getset1)
46+
self.assertIsNone(t.getset2)
47+
t.getset1 = 5
48+
sentinel = object()
49+
t.getset2 = sentinel
50+
self.assertIs(_testcapi.fpcast_last_set_value(), sentinel)
51+
with self.assertRaises(AttributeError):
52+
t.getset0 = 1
53+
54+
@unittest.skipUnless(is_wasm32, "requires the wasm call trampoline")
55+
def test_unsupported_signature(self):
56+
# A function with four pointer arguments matches none of the
57+
# signatures the trampoline knows about: it must raise SystemError
58+
# rather than trap.
59+
t = _testcapi.FpcastTestType()
60+
with self.assertRaises(SystemError):
61+
_testcapi.fpcast_noargs4()
62+
with self.assertRaises(SystemError):
63+
t.noargs4()
64+
with self.assertRaises(SystemError):
65+
_testcapi.FpcastCallable4()()
66+
with self.assertRaises(SystemError):
67+
t.getset4
68+
with self.assertRaises(SystemError):
69+
t.getset4 = 1
70+
71+
72+
if __name__ == "__main__":
73+
unittest.main()

‎Makefile.pre.in‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,6 +1439,7 @@ PYTHON_HEADERS= \
14391439
$(srcdir)/Include/internal/pycore_uop.h \
14401440
$(srcdir)/Include/internal/pycore_uop_ids.h \
14411441
$(srcdir)/Include/internal/pycore_uop_metadata.h \
1442+
$(srcdir)/Include/internal/pycore_wasm_trampoline.h \
14421443
$(srcdir)/Include/internal/pycore_warnings.h \
14431444
$(srcdir)/Include/internal/pycore_weakref.h \
14441445
$(DTRACE_HEADERS) \
@@ -3153,13 +3154,6 @@ Python/asm_trampoline_universal2.o: $(srcdir)/Python/asm_trampoline_aarch64.S $(
31533154
rm -f Python/asm_trampoline_arm64-apple-darwin.o \
31543155
Python/asm_trampoline_x86_64-apple-darwin.o
31553156

3156-
Python/emscripten_trampoline_inner.wasm: $(srcdir)/Python/emscripten_trampoline_inner.c
3157-
# emcc has a path that ends with emsdk/upstream/emscripten/emcc, we're looking for emsdk/upstream/bin/clang.
3158-
$$(em-config LLVM_ROOT)/clang -o $@ $< -mgc -O2 -Wl,--no-entry -Wl,--import-table -Wl,--import-memory -target wasm32-unknown-unknown -nostdlib
3159-
3160-
Python/emscripten_trampoline_wasm.c: Python/emscripten_trampoline_inner.wasm
3161-
$(PYTHON_FOR_REGEN) $(srcdir)/Platforms/emscripten/prepare_external_wasm.py $< $@ getWasmTrampolineModule
3162-
31633157
JIT_SHIM_BUILD_OBJS= @JIT_SHIM_BUILD_O@
31643158
JIT_UNWIND_INFO_H= $(if $(JIT_OBJS),jit_unwind_info.h $(patsubst jit_stencils-%.h,jit_unwind_info-%.h,@JIT_STENCILS_H@))
31653159
JIT_BUILD_TARGETS= jit_stencils.h @JIT_STENCILS_H@ $(JIT_UNWIND_INFO_H) $(JIT_SHIM_BUILD_OBJS)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Added support for function call adaptor trampolines for wasi to prevent
2+
traps when C handlers take the wrong number of arguments. Dropped Emscripten
3+
function call adaptors support for JS runtimes that don't support wasm-gc.
4+
5+
#.. section: Windows #.. section: macOS #.. section: IDLE #.. section:
6+
Tools/Demos #.. section: C API
7+
8+
# Write your Misc/NEWS.d entry below. It should be a simple ReST paragraph.
9+
# Don't start with "- Issue #<n>: " or "- gh-issue-<n>: " or that sort of
10+
stuff.
11+
###########################################################################

‎Modules/Setup.stdlib.in‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@
173173
@MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c
174174
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
175175
@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
176-
@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
176+
@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
177177
@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
178178
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
179179
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c

0 commit comments

Comments
 (0)