You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The short version: having syrupy installed appears to greatly increase the chances of a segmentation fault during multi-threaded browser testing using playwright.
The long version:
syrupy's pytest_assertrepr_compare hook always calls __terminal_color even though it doesn't need to most of the time (unless both operands are SnapshotAssertion)
__terminal_coloralways calls env_context even though it doesn't need to unless --snapshot-no-colors is enabled
therefore the process can segfault if syrupy updates the environment at the same time as another thread (e.g. a request handler from pytest-django live_server fixture) ends up calling getenv from native code (e.g. during database connection in my case)
To reproduce
Because it is a threading race there is no 100% repro, but in my case it happened approx 1 in 10 times during execution of a test suite of 235 playwright browser integration tests.
This is not an open source codebase so I'm unfortunately unable to provide an example project.
However patching syrupy to not call env_context and then running this same playwright test suite repeatedly (200 times!) overnight it did not segfault at all - whereas previously it would reliably reproduce within 5-15 runs.
Also this was first seen in our CI automation within 1 day after the PR which added syrupy was merged so there is a strong correlation.
Expected behavior
Simply installing syrupy should not cause intermittent segfaults in unrelated code.
Environment
OS: Amazon Linux 2023 Docker container
Syrupy Version: 4.7.1
Python Version: 3.12.9
Further details
In order to help with googling for this issue, instead of screenshots here are textual stack traces (both python faulthandler stacks at exit and also gdb backtrace from the segfault core dump)
GDB backtrace from the segfault, showing the getenv call which died:
(gdb) bt
#0 0x00007febf2b1e57c in __pthread_kill_implementation () from /lib64/libc.so.6
#1 0x00007febf2ad1d36 in raise () from /lib64/libc.so.6
#2 <signal handler called>
#3 0x00007febf2ad3c1d in getenv () from /lib64/libc.so.6
#4 0x00007febed87fbb4 in k5_init_trace () from /path/to/python/lib/python3.12/site-packages/psycopg2/../psycopg2_binary.libs/libkrb5-fcafa220.so.3.3
#5 0x00007febed84dd68 in krb5_init_context_profile () from /path/to/python/lib/python3.12/site-packages/psycopg2/../psycopg2_binary.libs/libkrb5-fcafa220.so.3.3
#6 0x00007febedb80d92 in krb5_gss_acquire_cred_from () from /path/to/python/lib/python3.12/site-packages/psycopg2/../psycopg2_binary.libs/libgssapi_krb5-497db0c6.so.2.2
#7 0x00007febedb6d160 in gss_add_cred_from () from /path/to/python/lib/python3.12/site-packages/psycopg2/../psycopg2_binary.libs/libgssapi_krb5-497db0c6.so.2.2
#8 0x00007febedb6d7a9 in gss_acquire_cred_from () from /path/to/python/lib/python3.12/site-packages/psycopg2/../psycopg2_binary.libs/libgssapi_krb5-497db0c6.so.2.2
#9 0x00007febedb6d9c4 in gss_acquire_cred () from /path/to/python/lib/python3.12/site-packages/psycopg2/../psycopg2_binary.libs/libgssapi_krb5-497db0c6.so.2.2
#10 0x00007febee177320 in ?? () from /path/to/python/lib/python3.12/site-packages/psycopg2/../psycopg2_binary.libs/libpq-e8a033dd.so.5.16
#11 0x00007febee161df4 in PQconnectPoll () from /path/to/python/lib/python3.12/site-packages/psycopg2/../psycopg2_binary.libs/libpq-e8a033dd.so.5.16
#12 0x00007febee162734 in ?? () from /path/to/python/lib/python3.12/site-packages/psycopg2/../psycopg2_binary.libs/libpq-e8a033dd.so.5.16
#13 0x00007febee165b68 in PQconnectdb () from /path/to/python/lib/python3.12/site-packages/psycopg2/../psycopg2_binary.libs/libpq-e8a033dd.so.5.16
#14 0x00007febf1957558 in ?? () from /path/to/python/lib/python3.12/site-packages/psycopg2/_psycopg.cpython-312-x86_64-linux-gnu.so
#15 0x00007febf19587c0 in ?? () from /path/to/python/lib/python3.12/site-packages/psycopg2/_psycopg.cpython-312-x86_64-linux-gnu.so
#16 0x00000000005452fc in type_call (type=<optimized out>, type@entry=0x7febf1989420, args=args@entry=0x7febd7a927a0, kwds=kwds@entry=0x0) at Objects/typeobject.c:1679
#17 0x000000000051764a in _PyObject_MakeTpCall (tstate=0x7febe0016bd0, callable=0x7febf1989420, args=0x7febd72fbb50, nargs=1, keywords=0x0) at Objects/call.c:240
#18 0x0000000000517458 in _PyObject_VectorcallTstate (kwnames=0x0, nargsf=<optimized out>, args=0x7febd72fbb50, callable=0x7febf1989420, tstate=0x7febe0016bd0) at ./Include/internal/pycore_call.h:90
#19 _PyObject_VectorcallTstate (kwnames=0x0, nargsf=<optimized out>, args=0x7febd72fbb50, callable=0x7febf1989420, tstate=0x7febe0016bd0) at ./Include/internal/pycore_call.h:77
#20 _PyObject_CallFunctionVa (tstate=0x7febe0016bd0, callable=0x7febf1989420, format=<optimized out>, va=<optimized out>, is_size_t=<optimized out>) at Objects/call.c:562
#21 0x00000000005b0f04 in _PyObject_CallFunction_SizeT (callable=<optimized out>, format=<optimized out>) at Objects/call.c:616
#22 0x00007febf1967069 in ?? () from /path/to/python/lib/python3.12/site-packages/psycopg2/_psycopg.cpython-312-x86_64-linux-gnu.so
#23 0x000000000053a05b in cfunction_call (func=0x7febee268a40, args=<optimized out>, kwargs=<optimized out>) at Objects/methodobject.c:537
#24 0x0000000000518bcb in PyObject_Call ()
#25 0x000000000056974a in _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=0x7febef78cba0, throwflag=<optimized out>) at Python/bytecodes.c:3263
#26 0x0000000000519356 in _PyObject_VectorcallTstate (kwnames=0x0, nargsf=1, args=0x7febd72fbef8, callable=0x7febef1dc5e0, tstate=0x7febe0016bd0) at ./Include/internal/pycore_call.h:92
#27 method_vectorcall (method=method@entry=0x7febd75e6880, args=args@entry=0x0, nargsf=nargsf@entry=0, kwnames=kwnames@entry=0x0) at Objects/classobject.c:69
#28 0x00000000005699ee in _PyObject_VectorcallTstate (args=0x0, nargsf=0, kwnames=0x0, callable=0x7febd75e6880, tstate=0x7febe0016bd0) at ./Include/internal/pycore_call.h:92
#29 _PyObject_CallNoArgs (func=0x7febd75e6880) at ./Include/internal/pycore_call.h:108
#30 _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=0x7febef78c6d0, throwflag=<optimized out>) at Python/bytecodes.c:2515
#31 0x0000000000518406 in _PyFunction_Vectorcall (kwnames=0x0, nargsf=<optimized out>, stack=0x7febd72fc130, func=0x7febe6b0e340) at Objects/call.c:419
#32 _PyObject_FastCallDictTstate (kwargs=0x7febe00012e0, nargsf=<optimized out>, args=0x7febd72fc130, callable=0x7febe6b0e340, tstate=0x7febe0016bd0) at Objects/call.c:133
#33 _PyObject_Call_Prepend (tstate=tstate@entry=0x7febe0016bd0, callable=callable@entry=0x7febe6b0e340, obj=obj@entry=0x7febe6b1f470, args=args@entry=0x7febd7a90910, kwargs=kwargs@entry=0x0)
at Objects/call.c:508
#34 0x00000000005cfbab in slot_tp_call (self=self@entry=0x7febe6b1f470, args=args@entry=0x7febd7a90910, kwds=kwds@entry=0x0) at Objects/typeobject.c:8791
#35 0x000000000051764a in _PyObject_MakeTpCall (tstate=0x7febe0016bd0, callable=0x7febe6b1f470, args=0x7febef78c4b0, nargs=1, keywords=0x0) at Objects/call.c:240
#36 0x00000000005655fa in _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=0x7febef78c438, throwflag=<optimized out>) at Python/bytecodes.c:2715
#37 0x0000000000518406 in _PyFunction_Vectorcall (kwnames=0x0, nargsf=<optimized out>, stack=0x7febd72fc400, func=0x7febe9f65440) at Objects/call.c:419
...
(a bunch more _PyXYZ stack frames omitted)
...
#116 _PyObject_FastCallDictTstate (kwargs=<optimized out>, nargsf=<optimized out>, args=0x7febd72fe890, callable=0x7febee7409a0, tstate=0x7febe0016bd0) at Objects/call.c:133
#117 _PyObject_Call_Prepend (tstate=tstate@entry=0x7febe0016bd0, callable=callable@entry=0x7febee7409a0, obj=obj@entry=0x7febd795d130, args=args@entry=0x7febd78af8c0, kwargs=<optimized out>)
at Objects/call.c:508
#118 0x000000000054694c in slot_tp_init (self=0x7febd795d130, args=0x7febd78af8c0, kwds=<optimized out>) at Objects/typeobject.c:9035
#119 0x00000000005452fc in type_call (type=<optimized out>, type@entry=0x3745700, args=args@entry=0x7febd78af8c0, kwds=kwds@entry=0x0) at Objects/typeobject.c:1679
#120 0x000000000051764a in _PyObject_MakeTpCall (tstate=0x7febe0016bd0, callable=0x3745700, args=0x7febef78b318, nargs=3, keywords=0x0) at Objects/call.c:240
#121 0x00000000005655fa in _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=0x7febef78b2a8, throwflag=<optimized out>) at Python/bytecodes.c:2715
#122 0x0000000000519297 in _PyObject_VectorcallTstate (kwnames=<optimized out>, nargsf=3, args=0x7febd72feb90, callable=0x7febea2b56c0, tstate=0x7febe0016bd0) at ./Include/internal/pycore_call.h:92
#123 method_vectorcall (method=<optimized out>, args=<optimized out>, nargsf=<optimized out>, kwnames=<optimized out>) at Objects/classobject.c:91
#124 0x000000000056974a in _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=0x7febef78b110, throwflag=<optimized out>) at Python/bytecodes.c:3263
#125 0x0000000000519356 in _PyObject_VectorcallTstate (kwnames=0x0, nargsf=1, args=0x7febd72feda8, callable=0x7febf254aa20, tstate=0x7febe0016bd0) at ./Include/internal/pycore_call.h:92
#126 method_vectorcall (method=<optimized out>, args=0x9307c8 <_PyRuntime+75624>, nargsf=<optimized out>, kwnames=<optimized out>) at Objects/classobject.c:69
#127 0x0000000000635885 in thread_run (boot_raw=0x7febe0001c70) at ./Modules/_threadmodule.c:1114
#128 0x0000000000615ce4 in pythread_wrapper (arg=<optimized out>) at Python/thread_pthread.h:237
#129 0x00007febf2b1c832 in start_thread () from /lib64/libc.so.6
#130 0x00007febf2abc314 in clone () from /lib64/libc.so.6
(gdb)
Python stack traces at the time of the segfault:
the first thread is the one which ended up calling getenv
the last thread is in our test code handle_console_message which at line 154 is doing assert msg.type == "verbose" or page.title() == "Expected title" which fits with syrupy having recently executed pytest_assertrepr_compare for the first equality comparison
Fatal Python error: Segmentation fault
Current thread 0x00007febd72ff640 (most recent call first):
File "/path/to/python/lib/python3.12/site-packages/psycopg2/__init__.py", line 122 in connect
File "/path/to/python/lib/python3.12/site-packages/django/db/backends/postgresql/base.py", line 275 in get_new_connection
File "/path/to/python/lib/python3.12/site-packages/django/utils/asyncio.py", line 26 in inner
File "/path/to/python/lib/python3.12/site-packages/django/db/backends/base/base.py", line 270 in connect
File "/path/to/python/lib/python3.12/site-packages/django/utils/asyncio.py", line 26 in inner
File "/path/to/python/lib/python3.12/site-packages/django/db/backends/base/base.py", line 289 in ensure_connection
File "/path/to/python/lib/python3.12/site-packages/django/utils/asyncio.py", line 26 in inner
File "/path/to/python/lib/python3.12/site-packages/django/db/backends/base/base.py", line 464 in get_autocommit
File "/path/to/python/lib/python3.12/site-packages/django/db/transaction.py", line 198 in __enter__
File "/python/lib/python3.12/contextlib.py", line 80 in inner
File "/path/to/python/lib/python3.12/site-packages/django/core/handlers/base.py", line 197 in _get_response
...
... (some middleware frames omitted for brevity)
...
File "/path/to/python/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55 in inner
File "/path/to/python/lib/python3.12/site-packages/django/utils/deprecation.py", line 134 in __call__
File "/path/to/python/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55 in inner
File "/path/to/python/lib/python3.12/site-packages/django/core/handlers/base.py", line 140 in get_response
File "/path/to/python/lib/python3.12/site-packages/django/test/testcases.py", line 1709 in get_response
File "/path/to/python/lib/python3.12/site-packages/django/core/handlers/wsgi.py", line 124 in __call__
File "/path/to/python/lib/python3.12/site-packages/django/test/testcases.py", line 1723 in __call__
File "/path/to/python/lib/python3.12/site-packages/django/contrib/staticfiles/handlers.py", line 80 in __call__
File "/python/lib/python3.12/wsgiref/handlers.py", line 137 in run
File "/path/to/python/lib/python3.12/site-packages/django/core/servers/basehttp.py", line 252 in handle_one_request
File "/path/to/python/lib/python3.12/site-packages/django/core/servers/basehttp.py", line 229 in handle
File "/python/lib/python3.12/socketserver.py", line 766 in __init__
File "/python/lib/python3.12/socketserver.py", line 362 in finish_request
File "/python/lib/python3.12/socketserver.py", line 697 in process_request_thread
File "/path/to/python/lib/python3.12/site-packages/django/core/servers/basehttp.py", line 103 in process_request_thread
File "/python/lib/python3.12/threading.py", line 1012 in run
File "/python/lib/python3.12/threading.py", line 1075 in _bootstrap_inner
File "/python/lib/python3.12/threading.py", line 1032 in _bootstrap
Thread 0x00007febbe0fc640 (most recent call first):
File "/python/lib/python3.12/socket.py", line 720 in readinto
File "/path/to/python/lib/python3.12/site-packages/django/core/servers/basehttp.py", line 237 in handle_one_request
File "/path/to/python/lib/python3.12/site-packages/django/core/servers/basehttp.py", line 229 in handle
File "/python/lib/python3.12/socketserver.py", line 766 in __init__
File "/python/lib/python3.12/socketserver.py", line 362 in finish_request
File "/python/lib/python3.12/socketserver.py", line 697 in process_request_thread
File "/path/to/python/lib/python3.12/site-packages/django/core/servers/basehttp.py", line 103 in process_request_thread
File "/python/lib/python3.12/threading.py", line 1012 in run
File "/python/lib/python3.12/threading.py", line 1075 in _bootstrap_inner
File "/python/lib/python3.12/threading.py", line 1032 in _bootstrap
Thread 0x00007febe53d7640 (most recent call first):
File "/python/lib/python3.12/asyncio/unix_events.py", line 1408 in _do_waitpid
File "/python/lib/python3.12/threading.py", line 1012 in run
File "/python/lib/python3.12/threading.py", line 1075 in _bootstrap_inner
File "/python/lib/python3.12/threading.py", line 1032 in _bootstrap
Thread 0x00007febe5dd8640 (most recent call first):
File "/python/lib/python3.12/selectors.py", line 415 in select
File "/python/lib/python3.12/socketserver.py", line 235 in serve_forever
File "/python/lib/python3.12/threading.py", line 1012 in run
File "/python/lib/python3.12/threading.py", line 1075 in _bootstrap_inner
File "/python/lib/python3.12/threading.py", line 1032 in _bootstrap
Thread 0x00007febe6ad9640 (most recent call first):
File "/python/lib/python3.12/selectors.py", line 415 in select
File "/python/lib/python3.12/socketserver.py", line 235 in serve_forever
File "/path/to/python/lib/python3.12/site-packages/django/test/testcases.py", line 1787 in run
File "/python/lib/python3.12/threading.py", line 1075 in _bootstrap_inner
File "/python/lib/python3.12/threading.py", line 1032 in _bootstrap
Thread 0x00007febf2a7a740 (most recent call first):
File "/python/lib/python3.12/inspect.py", line 1078 in getsourcefile
File "/python/lib/python3.12/inspect.py", line 1718 in getframeinfo
File "/python/lib/python3.12/inspect.py", line 1756 in getouterframes
File "/python/lib/python3.12/inspect.py", line 1781 in stack
File "/path/to/python/lib/python3.12/site-packages/playwright/_impl/_sync_base.py", line 108 in _sync
File "/path/to/python/lib/python3.12/site-packages/playwright/sync_api/_generated.py", line 9572 in title
File "/path/to/our/tests/conftest.py", line 154 in handle_console_message
File "/path/to/python/lib/python3.12/site-packages/playwright/_impl/_impl_to_api_mapping.py", line 123 in wrapper_func
File "/path/to/python/lib/python3.12/site-packages/pyee/asyncio.py", line 51 in _emit_run
File "/path/to/python/lib/python3.12/site-packages/pyee/base.py", line 184 in _call_handlers
File "/path/to/python/lib/python3.12/site-packages/pyee/base.py", line 208 in emit
File "/path/to/python/lib/python3.12/site-packages/playwright/_impl/_browser_context.py", line 630 in _on_console_message
File "/path/to/python/lib/python3.12/site-packages/playwright/_impl/_browser_context.py", line 147 in <lambda>
File "/path/to/python/lib/python3.12/site-packages/playwright/_impl/_connection.py", line 430 in _listener_with_error_handler_attached
Extension modules: greenlet._greenlet, yaml._yaml, simplejson._speedups, msgpack._cmsgpack, bitarray._bitarray, bitarray._util, lxml._elementpath, lxml.etree, psycopg2._psycopg, _cffi_backend, markupsafe._speedups (total: 11)
Stack trace showing syrupy updating the environment (as described in the bullet points at the top)
See syrupy-project#955 for a
detailed explanation of why this can cause thread safety issues
resulting in a segfault when another thread calls `getenv`
sparrowt
added a commit
to sparrowt/syrupy
that referenced
this issue
Feb 20, 2025
If neither operand is a `SnapshotAssertion` then the rest of the
code is not going to do anything, so bail early rather than
unnecessarily setting up `__terminal_color` etc. which can cause
issues c.f. syrupy-project#955
I've opened #956 to significantly reduce the probability of this occurring, though as noted there the possibility is not entirely gone.
Perhaps a more watertight way to fix this is to avoid modifying os.environ at all and use a different mechanism of signalling whether terminal colouring is enabled or not, e.g. using contextvars?
* fix: __terminal_color shouldn't modify os.environ unless it has to
See #955 for a
detailed explanation of why this can cause thread safety issues
resulting in a segfault when another thread calls `getenv`
* fix: pytest_assertrepr_compare should be a no-op for other types
If neither operand is a `SnapshotAssertion` then the rest of the
code is not going to do anything, so bail early rather than
unnecessarily setting up `__terminal_color` etc. which can cause
issues c.f. #955
* chore: run linter
---------
Co-authored-by: Noah Ulster <noah.u@roserocket.com>
Describe the bug
The short version: having
syrupy
installed appears to greatly increase the chances of a segmentation fault during multi-threaded browser testing using playwright.The long version:
pytest_assertrepr_compare
hook always calls__terminal_color
even though it doesn't need to most of the time (unless both operands areSnapshotAssertion
)__terminal_color
always callsenv_context
even though it doesn't need to unless--snapshot-no-colors
is enabledenv_context
callsos.environ.update
os.environ
is not thread safe in glibc see:live_server
fixture) ends up callinggetenv
from native code (e.g. during database connection in my case)To reproduce
Because it is a threading race there is no 100% repro, but in my case it happened approx 1 in 10 times during execution of a test suite of 235 playwright browser integration tests.
This is not an open source codebase so I'm unfortunately unable to provide an example project.
However patching syrupy to not call
env_context
and then running this same playwright test suite repeatedly (200 times!) overnight it did not segfault at all - whereas previously it would reliably reproduce within 5-15 runs.Also this was first seen in our CI automation within 1 day after the PR which added syrupy was merged so there is a strong correlation.
Expected behavior
Simply installing syrupy should not cause intermittent segfaults in unrelated code.
Environment
Further details
In order to help with googling for this issue, instead of screenshots here are textual stack traces (both python faulthandler stacks at exit and also gdb backtrace from the segfault core dump)
GDB backtrace from the segfault, showing the
getenv
call which died:Python stack traces at the time of the segfault:
handle_console_message
which at line 154 is doingassert msg.type == "verbose" or page.title() == "Expected title"
which fits withsyrupy
having recently executedpytest_assertrepr_compare
for the first equality comparisonStack trace showing
syrupy
updating the environment (as described in the bullet points at the top)The text was updated successfully, but these errors were encountered: