From 9af185afeaa7928dd8efe5c24029d0f5867377cc Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Tue, 15 Sep 2026 09:20:52 -0300 Subject: [PATCH] Emscripten: take a signal from the signal buffer in one step The helper read the signal buffer and then cleared it as two separate operations. The buffer is shared with another thread, so a signal written between the read and the clear was overwritten and never delivered. Atomics.exchange() reads and clears it atomically. Add a test that a signal in a shared or non-shared buffer is delivered once and the buffer is left cleared. Co-authored-by: Claude Opus 5 (1M context) --- Lib/test/test_capi/test_emscripten.py | 31 +++++++++++++++++- ...-09-15-09-18-51.gh-issue-157548.cq9Iu2.rst | 2 ++ Modules/_testinternalcapi.c | 32 +++++++++++++++++++ Python/emscripten_signal.c | 4 +-- 4 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-09-15-09-18-51.gh-issue-157548.cq9Iu2.rst diff --git a/Lib/test/test_capi/test_emscripten.py b/Lib/test/test_capi/test_emscripten.py index 272d9a10ceb950a..1d3ec495d8fe334 100644 --- a/Lib/test/test_capi/test_emscripten.py +++ b/Lib/test/test_capi/test_emscripten.py @@ -1,10 +1,14 @@ +import signal import unittest from test.support import is_emscripten if not is_emscripten: raise unittest.SkipTest("Emscripten-only test") -from _testinternalcapi import emscripten_set_up_async_input_device +from _testinternalcapi import ( + emscripten_check_signal_buffer, + emscripten_set_up_async_input_device, +) from pathlib import Path @@ -23,3 +27,28 @@ def test_emscripten_async_input_device(self): self.assertEqual(f.readline().strip(), "ab") self.assertEqual(f.readline().strip(), "fi") self.assertEqual(f.readline().strip(), "xy") + + +class EmscriptenSignalBufferTest(unittest.TestCase): + def check_signal_buffer(self, shared): + received = [] + + def handler(signum, frame): + received.append(signum) + + old_handler = signal.signal(signal.SIGUSR1, handler) + self.addCleanup(signal.signal, signal.SIGUSR1, old_handler) + + left_in_buffer = emscripten_check_signal_buffer(signal.SIGUSR1, shared) + for _ in range(1000): + if received: + break + + self.assertEqual(left_in_buffer, 0) + self.assertEqual(received, [signal.SIGUSR1]) + + def test_shared_buffer(self): + self.check_signal_buffer(shared=True) + + def test_unshared_buffer(self): + self.check_signal_buffer(shared=False) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-15-09-18-51.gh-issue-157548.cq9Iu2.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-15-09-18-51.gh-issue-157548.cq9Iu2.rst new file mode 100644 index 000000000000000..4eae35f79fe71d1 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-15-09-18-51.gh-issue-157548.cq9Iu2.rst @@ -0,0 +1,2 @@ +On Emscripten, a signal written to the signal buffer while the interpreter +was clearing it is no longer lost. diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 38e56ae70420985..76408c3531aed28 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -2945,6 +2945,7 @@ incref_decref_delayed(PyObject *self, PyObject *op) #ifdef __EMSCRIPTEN__ #include "emscripten.h" +#include "pycore_emscripten_signal.h" // Py_EMSCRIPTEN_SIGNAL_HANDLING EM_JS(int, emscripten_set_up_async_input_device_js, (void), { let idx = 0; @@ -2972,6 +2973,36 @@ emscripten_set_up_async_input_device(PyObject *self, PyObject *Py_UNUSED(ignored Py_RETURN_FALSE; } } + +EM_JS(void, emscripten_set_signal_buffer_js, (int signum, int shared), { + Module._Py_saved_signal_buffer = Module.Py_EmscriptenSignalBuffer; + Module.Py_EmscriptenSignalBuffer = shared + ? new Uint8Array(new SharedArrayBuffer(1)) + : new Uint8Array(1); + Module.Py_EmscriptenSignalBuffer[0] = signum; +}); + +EM_JS(int, emscripten_restore_signal_buffer_js, (void), { + const value = Module.Py_EmscriptenSignalBuffer[0]; + Module.Py_EmscriptenSignalBuffer = Module._Py_saved_signal_buffer; + delete Module._Py_saved_signal_buffer; + return value; +}); + +static PyObject * +emscripten_check_signal_buffer(PyObject *self, PyObject *args) +{ + int signum, shared; + if (!PyArg_ParseTuple(args, "ip", &signum, &shared)) { + return NULL; + } + int handling = Py_EMSCRIPTEN_SIGNAL_HANDLING; + emscripten_set_signal_buffer_js(signum, shared); + Py_EMSCRIPTEN_SIGNAL_HANDLING = 1; + _Py_CheckEmscriptenSignals(); + Py_EMSCRIPTEN_SIGNAL_HANDLING = handling; + return PyLong_FromLong(emscripten_restore_signal_buffer_js()); +} #endif static PyObject * @@ -3383,6 +3414,7 @@ static PyMethodDef module_functions[] = { GET_NEXT_DICT_KEYS_VERSION_METHODDEF #ifdef __EMSCRIPTEN__ {"emscripten_set_up_async_input_device", emscripten_set_up_async_input_device, METH_NOARGS}, + {"emscripten_check_signal_buffer", emscripten_check_signal_buffer, METH_VARARGS}, #endif {"simple_pending_call", simple_pending_call, METH_O}, {"set_vectorcall_nop", set_vectorcall_nop, METH_O}, diff --git a/Python/emscripten_signal.c b/Python/emscripten_signal.c index 561b5b73cd6b709..4aeb6a2997fe3fa 100644 --- a/Python/emscripten_signal.c +++ b/Python/emscripten_signal.c @@ -13,9 +13,7 @@ EM_JS(int, _Py_CheckEmscriptenSignals_Helper, (void), { return 0; } try { - let result = Module.Py_EmscriptenSignalBuffer[0]; - Module.Py_EmscriptenSignalBuffer[0] = 0; - return result; + return Atomics.exchange(Module.Py_EmscriptenSignalBuffer, 0, 0); } catch(e) { #if !defined(NDEBUG) console.warn("Error occurred while trying to read signal buffer:", e);