Skip to content

Heisenbug that kills process via SIGALRM #133687

Description

@johndoe31415

Bug report

Bug description:

First of all, I know exactly how this bug is going to sound: batshit insane. I implore you to keep reading.

I've had a timer application for ages that uses signal.alarm() concurrently with input() so that it has a rudimentary way of displaying things in the background while still allowing user interaction on the CLI. The gist of it is this:

@staticmethod
def _input_timeout(prompt, timeout_secs):
	class TimeExpiredException(Exception): pass

	def raise_exception(signum, frame):
		raise TimeExpiredException()

	signal.signal(signal.SIGALRM, raise_exception)
	try:
		signal.alarm(timeout_secs)
		result = input(prompt)
		signal.alarm(0)
	except TimeExpiredException:
		result = None
	return result

This program has mysteriously stopped working with Python 3.13.3 with an exceptionally strange error mode: my signal handler is called from somewhere I apparently do not expect (HelpFormatter), causing the whole process to terminate. This always happens after exactly 29 seconds when I run my process as user and after exactly 25 seconds when I run as root. It happens only when there is no __pycache__ present, i.e., when the Python bytecode is generated on startup.

I tried creating a minimal reproducer, which was super difficult. Even when I remove dead code from the Python script, the bug stops triggering. Some code removal was fine, but for example if I remove the wrapper around argparse, the bug disappears.

As I said. This sounds batshit crazy.

When the mystery signal is caught, it gives one final indication where it comes from:

reliant [/tmp/heisenbug]: rm -fr __pycache__/ && time ./stopwatch 
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Exception ignored in tp_clear of HelpFormatter:
Traceback (most recent call last):
  File "/tmp/heisenbug/./stopwatch", line 69, in raise_exception
    raise TimeExpiredException()
StopWatch._input_timeout.<locals>.TimeExpiredException: 
FINALLY

real	0m25,035s
user	0m0,025s
sys	0m0,011s

Note the message: Exception ignored in tp_clear of HelpFormatter: (this was running as root, you see that it took 25 seconds.)

Just so you believe me that it always triggers after the same time, here are three consecutive runs as root (I swear this is not copy paste, timings are accurate to millisecond level!):

reliant [/tmp/heisenbug]: rm -fr __pycache__/ && time ./stopwatch 
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Exception ignored in tp_clear of HelpFormatter:
Traceback (most recent call last):
  File "/tmp/heisenbug/./stopwatch", line 69, in raise_exception
    raise TimeExpiredException()
StopWatch._input_timeout.<locals>.TimeExpiredException: 
FINALLY

real	0m25,035s
user	0m0,025s
sys	0m0,011s

reliant [/tmp/heisenbug]: rm -fr __pycache__/ && time ./stopwatch 
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Exception ignored in tp_clear of HelpFormatter:
Traceback (most recent call last):
  File "/tmp/heisenbug/./stopwatch", line 69, in raise_exception
    raise TimeExpiredException()
StopWatch._input_timeout.<locals>.TimeExpiredException: 
FINALLY

real	0m25,035s
user	0m0,025s
sys	0m0,011s

reliant [/tmp/heisenbug]: rm -fr __pycache__/ && time ./stopwatch 
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Exception ignored in tp_clear of HelpFormatter:
Traceback (most recent call last):
  File "/tmp/heisenbug/./stopwatch", line 69, in raise_exception
    raise TimeExpiredException()
StopWatch._input_timeout.<locals>.TimeExpiredException: 
FINALLY

real	0m25,035s
user	0m0,025s
sys	0m0,010s

And here some runs as user:

reliant joe [/tmp/heisenbug]: rm -fr __pycache__ && time ./stopwatch 
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Exception ignored in tp_clear of HelpFormatter:
Traceback (most recent call last):
  File "/tmp/heisenbug/./stopwatch", line 69, in raise_exception
    raise TimeExpiredException()
StopWatch._input_timeout.<locals>.TimeExpiredException: 
FINALLY

real	0m29,038s
user	0m0,029s
sys	0m0,010s

reliant joe [/tmp/heisenbug]: rm -fr __pycache__ && time ./stopwatch 
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Exception ignored in tp_clear of HelpFormatter:
Traceback (most recent call last):
  File "/tmp/heisenbug/./stopwatch", line 69, in raise_exception
    raise TimeExpiredException()
StopWatch._input_timeout.<locals>.TimeExpiredException: 
FINALLY

real	0m29,039s
user	0m0,027s
sys	0m0,012s

reliant joe [/tmp/heisenbug]: rm -fr __pycache__ && time ./stopwatch 
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Exception ignored in tp_clear of HelpFormatter:
Traceback (most recent call last):
  File "/tmp/heisenbug/./stopwatch", line 69, in raise_exception
    raise TimeExpiredException()
StopWatch._input_timeout.<locals>.TimeExpiredException: 
FINALLY

real	0m29,038s
user	0m0,029s
sys	0m0,009s

I'm running on 6.14.0-15-generic #15-Ubuntu. My CPU is an AMD Ryzen Threadripper PRO 3945WX 12-Cores. I'm using the Ubuntu-provided Python package (Ubuntu calls it 3.13.3-1):

Python 3.13.3 (main, Apr  8 2025, 19:55:40) [GCC 14.2.0] on linux

I cannot reproduce this issue on Debian with Python 3.11/ARM nor on another Ubuntu with Python 3.12/x86_64.

I cannot attach files (the reproducer) to this bug, so here it is as base64:

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
3dZt3dZt3dZt3dZt3dZt3faN2v8Au7ZfrgBQAAA=

You can simply unpack it by doing:

$ openssl base64 -d | tar xvz

And pasting the above base64 blob in. Remember you have to clear __pycache__ to trigger the bug. I'm including the bytecode in case it contains something interesting.

Please someone tell me this is reproducible and I'm not completely losing my marbles.

CPython versions tested on:

3.13

Operating systems tested on:

Linux

Activity

  1. changed the title [-]Heisenbug that kills process via SIGALARM[/-] [+]Heisenbug that kills process via SIGALRM[/+] on May 8, 2025
  2. colesbury commented on May 8, 2025

    @colesbury
    Contributor

    I haven't looked at the reproducer yet, but this is understandable if unexpected behavior. Exceptions raised during destructors (i.e., __del__ methods) get printed as unraisable exceptions and then swallowed:

    https://docs.python.org/3/library/sys.html#sys.unraisablehook

    That's not new behavior and it's pretty much a fundamental limitation of raising exception asynchronously, such as with signal.signal(). Most likely, the timing of things slightly changed in 3.13 such that you routinely get "unlucky" as to what's executing when the SIGALRM triggers.

  3. johndoe31415 commented on May 8, 2025

    @johndoe31415
    Author

    Interesting, so are you saying that raising an exception is always undefined behavior in Python, always has been?

    The thing that makes me quite curious is that I do believe that in the scenario, this does look like it is well-defined behavior. I'm setting signal.alarm() inside the try block, then cancel it before leaving the try block. Any SIGALRM should call the installed signal handler and the raised exception should be possible to stack-unwind (there is only a single main thread there). In this instance, it seems to be the case that the signal is delivered while outside the try...except block, which should never be possible.

    Can you try to elaborate why this would be happening?

    I would be fine with the exception being omitted (e.g., during __del__, as you described) and being silently discarded. But somehow my program is not always able to catch the raised exception, which is indeed a problem.

    Thanks for taking the time to read through my report! Cheers.

  4. added
    pendingThe issue will be closed if no feedback is provided
    on May 8, 2025
  5. ronaldoussoren commented on May 10, 2025

    @ronaldoussoren
    Contributor

    Interesting, so are you saying that raising an exception is always undefined behavior in Python, always has been?
    No, exceptions are defined behaviour.

    There are signals involved here, and those result in asynchronous code execution in C. Integration of signal handlers in the rest of the interpreter is non-trivial.

    Anyway:

    • I can reproduce the issue on macOS (in about 25 seconds)
    • The proces is killed by SIGALRM reliably when using python3.13t (generally on the first iteration of the loop)
    • I cannot reproduce the issue with Python 3.14b1 (both regular and free-threading)
  6. johndoe31415 commented on May 19, 2025

    @johndoe31415
    Author

    First of all, thanks @ronaldoussoren for trying it out and confirming that there is indeed an issue. I agree that signal handling is exceptionally tricky and I'll be the first to admit that stack unwinding in a signal handler is just nasty. That said, if it's not UB, then it's an interpreter bug.

    I did some more digging and while alarm(2) allows only for second granularity, setitimer(2) does not -- and also has a helpful Python wrapper. With that I could boil down a minimal reproducer that triggers fairly quickly:

    #!/usr/bin/env python3
    import signal
    
    def _input_timeout(prompt):
    	class TimeExpiredException(Exception): pass
    
    	def raise_exception(signum, frame):
    		raise TimeExpiredException()
    
    	signal.signal(signal.SIGALRM, raise_exception)
    	try:
    		signal.setitimer(0, 0.01)
    		result = input(prompt)
    		signal.setitimer(0, 0)
    	except TimeExpiredException:
    		result = None
    	return result
    
    cnt = 0
    while True:
    	cnt += 1
    	_input_timeout(str(cnt))
    	print()
    

    Also note that while I said I could not reproduce on Python 3.12, I absolutely can now! Case in point:

    Linux sz 6.11.0-25-generic #25-Ubuntu SMP PREEMPT_DYNAMIC Fri Apr 11 23:29:18 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
    AMD Ryzen 7 2700X Eight-Core Processor
    Python 3.12.7 (main, Feb  4 2025, 14:46:03) [GCC 14.2.0] on linux
    
    $ ./heisenbug.py
    16
    17
    18Exception ignored in tp_clear of: <class 'cell'>
    Traceback (most recent call last):
      File "/home/joe/./heisenbug.py", line 8, in raise_exception
        raise TimeExpiredException()
    _input_timeout.<locals>.TimeExpiredException: 
    Traceback (most recent call last):
      File "/home/joe/./heisenbug.py", line 22, in <module>
        _input_timeout(str(cnt))
      File "/home/joe/./heisenbug.py", line 13, in _input_timeout
        result = input(prompt)
                 ^^^^^^^^^^^^^
    KeyboardInterrupt
    

    and

    Linux reliant 6.14.0-15-generic #15-Ubuntu SMP PREEMPT_DYNAMIC Sun Apr  6 15:05:05 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
    AMD Ryzen Threadripper PRO 3945WX 12-Cores
    Python 3.13.3 (main, Apr  8 2025, 19:55:40) [GCC 14.2.0] on linux
    $ ./heisenbug.py
    [...]
    79
    80
    81
    82
    83Exception ignored in tp_clear of dict:
    Traceback (most recent call last):
      File "/home/joe/./heisenbug.py", line 8, in raise_exception
        raise TimeExpiredException()
    _input_timeout.<locals>.TimeExpiredException: 
    Traceback (most recent call last):
      File "/home/joe/./heisenbug.py", line 22, in <module>
        _input_timeout(str(cnt))
        ~~~~~~~~~~~~~~^^^^^^^^^^
      File "/home/joe/./heisenbug.py", line 13, in _input_timeout
        result = input(prompt)
    KeyboardInterrupt
    

    and

    Linux julia 6.8.0-60-generic #63-Ubuntu SMP PREEMPT_DYNAMIC Tue Apr 15 19:04:15 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
    Intel(R) Core(TM) i7-6800K CPU @ 3.40GHz
    Python 3.12.3 (main, Feb  4 2025, 14:48:35) [GCC 13.3.0] on linux
    29
    30
    31
    32Exception ignored in tp_clear of: <class 'type'>
    Traceback (most recent call last):
      File "/home/julia/./heisenbug.py", line 8, in raise_exception
        raise TimeExpiredException()
    _input_timeout.<locals>.TimeExpiredException: 
    Traceback (most recent call last):
      File "/home/julia/./heisenbug.py", line 22, in <module>
        _input_timeout(str(cnt))
      File "/home/julia/./heisenbug.py", line 13, in _input_timeout
        result = input(prompt)
                 ^^^^^^^^^^^^^
    KeyboardInterrupt
    

    Note that I cannot produce the issue on an ARM64 virutal machine (did not trigger up until a counter value of 40000, when I aborted).

  7. johndoe31415 commented on May 25, 2025

    @johndoe31415
    Author

    So I did a lot of building and testing. Using this script:

    #!/usr/bin/env python3
    import signal
    
    def _input_timeout(prompt: str):
    	class TimeExpiredException(Exception): pass
    
    	def raise_exception(signum, frame):
    		raise TimeExpiredException()
    
    	signal.signal(signal.SIGALRM, raise_exception)
    	try:
    		signal.setitimer(0, 0.0001)
    		result = input(prompt)
    		signal.setitimer(0, 0)
    	except TimeExpiredException:
    		result = None
    	return result
    
    cnt = 0
    while True:
    	cnt += 1
    	_input_timeout(str(cnt))
    	print()
    	if cnt == 50000:
    		break
    

    I'm able to discern that a version is affected (return code != 0) or probably not affected (return code == 0). Note that since this is a nondeterministic issue, I can only ever verify presence of the issue, but not falsify it. This makes my analysis a bit messy.

    First, I checked the 3.12, 3.13, 3.14 branches and all versions that have tags for the issue:

    Ref v3.11.0a2 checked: good
    Ref v3.11.0a3 checked: good
    Ref v3.11.0a4 checked: good
    Ref v3.11.0a5 checked: good
    Ref v3.11.0a6 checked: good
    Ref v3.11.0a7 checked: good
    Ref v3.11.0b1 checked: good
    Ref v3.12.0a1 checked: good
    Ref v3.12.0a2 checked: good
    Ref v3.12.0a3 checked: good
    Ref v3.12.0a4 checked: good
    Ref v3.12.0a5 checked: good
    Ref v3.12.0a6 checked: good
    Ref v3.12.0a7 checked: good
    Ref v3.12.0b1 checked: good
    Ref v3.12.0b2 checked: good
    Ref v3.12.0b3 checked: good
    Ref v3.12.0b4 checked: good
    Ref v3.12.0rc1 checked: good
    Ref v3.12.0rc2 checked: good
    Ref v3.12.0rc3 checked: good
    Ref v3.12.0 checked: good
    Ref v3.12.1 checked: good
    Ref v3.12.2 checked: bad
    Ref v3.12.3 checked: bad
    Ref v3.12.4 checked: bad
    Ref v3.12.5 checked: bad
    Ref v3.12.6 checked: bad
    Ref v3.12.7 checked: bad
    Ref v3.12.8 checked: bad
    Ref v3.12.9 checked: bad
    Ref v3.12.10 checked: bad
    
    Ref v3.13.0a1 checked: skip
    Ref v3.13.0a2 checked: skip
    Ref v3.13.0a3 checked: skip
    Ref v3.13.0a4 checked: skip
    Ref v3.13.0a5 checked: skip
    Ref v3.13.0a6 checked: good
    Ref v3.13.0b1 checked: good
    Ref v3.13.0b2 checked: good
    Ref v3.13.0b3 checked: good
    Ref v3.13.0b4 checked: good
    Ref v3.13.0rc1 checked: good
    Ref v3.13.0rc2 checked: good
    Ref v3.13.0rc3 checked: bad
    Ref v3.13.0 checked: bad
    Ref v3.13.1 checked: bad
    Ref v3.13.2 checked: bad
    Ref v3.13.3 checked: bad
    
    Ref v3.14.0a1 checked: good
    Ref v3.14.0a2 checked: good
    Ref v3.14.0a3 checked: good
    Ref v3.14.0a4 checked: good
    Ref v3.14.0a5 checked: good
    Ref v3.14.0a6 checked: good
    Ref v3.14.0a7 checked: good
    Ref v3.14.0b1 checked: good
    

    I built each version with ./configure --enable-optimizations --disable-test-modules. So a GIL should always be present.

    Then I bisected in each branch that has the issue occuring. For 3.13, the faulty commit is plausible:

    # status: waiting for both good and bad commits
    # good: [ec610069637d56101896803a70d418a89afe0b4b] Python 3.13.0rc2
    git bisect good ec610069637d56101896803a70d418a89afe0b4b
    # status: waiting for bad commit, 1 good commit known
    # bad: [fae84c70fbd9ac391ec0d3826cc114a0036a71e1] Python 3.13.0rc3
    git bisect bad fae84c70fbd9ac391ec0d3826cc114a0036a71e1
    # good: [c2aaf507801e34c574a10ea5f079a8260143ef9a] [3.13] gh-123560: Correct docs for "empty" format type for floats (GH-123561) (#124596)
    git bisect good c2aaf507801e34c574a10ea5f079a8260143ef9a
    # good: [c7f9332ee51f612899d817a927f706216287dd70] [3.13] gh-124378: Update test_ttk for Tcl/Tk 8.6.15 (GH-124542) (#124544)
    git bisect good c7f9332ee51f612899d817a927f706216287dd70
    # good: [793cb77f55ac157cf3f32da863f39c408b29d78c] [3.13] gh-123942: add missing test for docstring-handling code in ast_opt.c (GH-123943) (#123955)
    git bisect good 793cb77f55ac157cf3f32da863f39c408b29d78c
    # good: [ce0eaa67032389d64fd2d7fb510e34372082eef8] [3.13] gh-124722: Fix leak in `test_detach_materialized_dict_no_memory` (GH-124769) (#124777)
    git bisect good ce0eaa67032389d64fd2d7fb510e34372082eef8
    # good: [4f3e96f7c25c20850a7512e127045ff82d8b57f7] [3.13] gh-124613, regrtest: Detect JIT in build info (GH-124793) (#124797)
    git bisect good 4f3e96f7c25c20850a7512e127045ff82d8b57f7
    # bad: [e0eb44ad49926dd131dc639f5506c6769e45b4eb] [3.13] GH-124567: Revert the Incremental GC in 3.13 (#124770)
    git bisect bad e0eb44ad49926dd131dc639f5506c6769e45b4eb
    # good: [bc1fae89af9df3888fab670f83b7aed8afe5a9f5] [3.13] Doc: Run HTML and non-HTML daily builds separately (GH-124493) (#124790)
    git bisect good bc1fae89af9df3888fab670f83b7aed8afe5a9f5
    # first bad commit: [e0eb44ad49926dd131dc639f5506c6769e45b4eb] [3.13] GH-124567: Revert the Incremental GC in 3.13 (#124770)
    

    For 3.12, the faulty commit is much less plausible and has me scratching my head:

    # status: waiting for both good and bad commits
    # good: [2305ca51448552542b2414186252123a8dc87db7] Python 3.12.1
    git bisect good 2305ca51448552542b2414186252123a8dc87db7
    # status: waiting for bad commit, 1 good commit known
    # bad: [6abddd9f6afdddc09031989e0deb25e301ecf315] Python 3.12.2
    git bisect bad 6abddd9f6afdddc09031989e0deb25e301ecf315
    # bad: [94b1d1fa38ada8cf7d196184a04a195c152eed75] [3.12] gh-114021: Pin various sphinxcontrib extensions to older versions (#114022)
    git bisect bad 94b1d1fa38ada8cf7d196184a04a195c152eed75
    # good: [4882d508be46eff5ae2d55d82f0aab169a59a0ed] [3.12] gh-74573: document that ndbm can silently corrupt databases on macOS (GH-113354) (#113431)
    git bisect good 4882d508be46eff5ae2d55d82f0aab169a59a0ed
    # bad: [d2cdc6717a4a03f5fc7a7b3a5d3e55973b1ff9d5] [3.12] gh-101100: Fix Sphinx warnings for removed dead batteries (GH-113669) (#113681)
    git bisect bad d2cdc6717a4a03f5fc7a7b3a5d3e55973b1ff9d5
    # bad: [6177a852c4b5dd82f40cef4340083a33cf16a5fd] [3.12] gh-101100: Fix Sphinx warnings in `library/cmd.rst` (GH-113502) (#113511)
    git bisect bad 6177a852c4b5dd82f40cef4340083a33cf16a5fd
    # bad: [bdad5c367f60d4939d6945f78d94608e4521009f] [3.12] gh-76785: Make test.test_interpreters executable (GH-112982) (GH-113470)
    git bisect bad bdad5c367f60d4939d6945f78d94608e4521009f
    # bad: [269cb342ad487c7c244659bd25ed6d4d7bb95876] [3.12] gh-113028: Correctly memoize str in pickle when escapes added (GH-113436) (GH-113448)
    git bisect bad 269cb342ad487c7c244659bd25ed6d4d7bb95876
    # good: [8f526d3c9a078982937b402e3519c3bcf721c4ad] [3.12] gh-109989: Fix test_c_locale_coercion when PYTHONIOENCODING is set (GH-113378) (#113398)
    git bisect good 8f526d3c9a078982937b402e3519c3bcf721c4ad
    # bad: [15ea4a429245ddb9d919757308777521f61c681c] [3.12] gh-111784: Fix two segfaults in the elementtree module (GH-113405) (GH-113446)
    git bisect bad 15ea4a429245ddb9d919757308777521f61c681c
    # bad: [7f2ebc84e8066e2c51feb6ef6403360922bbc0aa] [3.12] gh-112559: Avoid unnecessary conversion attempts to enum_klass in signal.py (GH-113040) (#113443)
    git bisect bad 7f2ebc84e8066e2c51feb6ef6403360922bbc0aa
    # first bad commit: [7f2ebc84e8066e2c51feb6ef6403360922bbc0aa] [3.12] gh-112559: Avoid unnecessary conversion attempts to enum_klass in signal.py (GH-113040) (#113443)
    

    Hope this helps a bit. Here are links to the faulty commits:

    Also note that because the faulty commit in 3.12 seems unlikely (most of the commit is testing code, the only change I can see is the type conversion to IntEnum), I did this bisection multiple times and always got the same results. I'd happily do more testing if this is useful.

  8. duaneg commented on Jun 15, 2025

    @duaneg
    Contributor

    Interesting, so are you saying that raising an exception is always undefined behavior in Python, always has been?

    Not undefined in the C sense, but in practice very difficult to use safely. The exception can be raised from ~anywhere, and code that is not specifically and very carefully written with that expectation will break. Standard library & garbage collector code is not written like this (and realistically cannot be), so reliably using a signal handler that raises involves, amongst other things, not calling into the standard library or allowing the garbage collector to run.

    The thing that makes me quite curious is that I do believe that in the scenario, this does look like it is well-defined behavior. I'm setting signal.alarm() inside the try block, then cancel it before leaving the try block. Any SIGALRM should call the installed signal handler and the raised exception should be possible to stack-unwind (there is only a single main thread there). In this instance, it seems to be the case that the signal is delivered while outside the try...except block, which should never be possible.

    Can you try to elaborate why this would be happening?

    I am not the person you asked, but I'll try to explain (and hopefully if I get anything wrong someone more knowledgeable will correct me)...

    In your example the signal will be delivered inside that try...except block, but the input call descends into interpreter internals and the standard library and will do a bunch of things, including possibly running the garbage collector, which is the source of the "ignored" error you are seeing. The garbage collector is calling tp_clear on some deleted garbage, then checking for an error. In this case, it finds one! The error was caused by the signal, not directly by the code it called, but it has no way to know that, and couldn't do anything useful with the information even if it did.

    GC has no way to gracefully handle such errors, so it prints that "ignored" message as a last ditch attempt to provide the user some indication as to why everything is going/about to go wrong. It cannot propagate the exception out and unwind the stack, since the garbage object which it thinks is throwing from tp_clear could have been allocated by some other totally unrelated code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    pendingThe issue will be closed if no feedback is providedtype-bugAn unexpected behavior, bug, or error

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions