LudovicRousseau / PyKCS11

PKCS#11 Wrapper for Python
GNU General Public License v2.0
98 stars 35 forks source link

KeyboardInterrupt appears blocked by waitForSlotEvent() #117

Open Torxed opened 4 months ago

Torxed commented 4 months ago

Your system information

Please describe your issue in as much detail as possible:

Using the events.py example, as referenced in #167, it appears to be working but blocking KeyboardInterrupt:

Traceback (most recent call last):
  File "/home/anton/test.py", line 15, in <module>
    slot = pkcs11.waitForSlotEvent()
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/PyKCS11/__init__.py", line 746, in waitForSlotEvent
    (rv, slot) = self.lib.C_WaitForSlotEvent(flags, tmp)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/PyKCS11/LowLevel.py", line 1808, in C_WaitForSlotEvent
    return _LowLevel.CPKCS11Lib_C_WaitForSlotEvent(self, flags, INOUT)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
KeyboardInterrupt

The above exception, appears to be blocked until systemctl stop pcscd.service is executed.

Steps for reproducing this issue:

import os
import PyKCS11

pkcs11 = PyKCS11.PyKCS11Lib()
if os.name == 'nt':
    pkcs11.load(r"C:\Program Files\OpenSC Project\OpenSC\pkcs11\opensc-pkcs11.dll")
else:
    pkcs11.load("/usr/lib/opensc-pkcs11.so")

slots = pkcs11.getSlotList()
print(slots)

while True:
    try:
        slot = pkcs11.waitForSlotEvent()
        print(slot)
    except PyKCS11.PyKCS11Error as e:
        print("Error:", e)

Pressing Ctrl-C will result in:

anton@hotlap ~ $ python test.py
[0]
^C^C^C^C

Until pcscd.service is restarted, then:

anton@hotlap ~ $ python test.py
[0]
^C^C^C^CTraceback (most recent call last):
  File "/home/anton/test.py", line 15, in <module>
    slot = pkcs11.waitForSlotEvent()
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/PyKCS11/__init__.py", line 746, in waitForSlotEvent
    (rv, slot) = self.lib.C_WaitForSlotEvent(flags, tmp)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/PyKCS11/LowLevel.py", line 1808, in C_WaitForSlotEvent
    return _LowLevel.CPKCS11Lib_C_WaitForSlotEvent(self, flags, INOUT)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
KeyboardInterrupt

anton@hotlap ~ $
LudovicRousseau commented 4 months ago

The KeyboardInterrupt is blocked until the call to C_WaitForSlotEvent() returns to Python. For example you can also insert or remove a token to get unblocked.

I don't know if it is possible to allow Ctrl-C from inside a C function called by Python. Do you have an idea?

Torxed commented 4 months ago

I believe the signal library would be able to hook the interrupt, from there you would need to break the C_WaitForSlotEvent() loop cleanly some how.

I haven't checked the low level C code yet, but could have a look :)

LudovicRousseau commented 4 months ago

The only way to interrupt a C_WaitForSlotEvent() is to call C_Finalize(). Maybe a signal handler could do that?

The problem is that the application that uses PyKCS11 may also want to get KeyboardInterrupt events.

Torxed commented 4 months ago

My C is very rusty, but instinctively I think builtin Python could handle this:

The end users code:

import time

# `b.py` will emulate PyKCS11 here
# just for the sake of testing signal() handling
# import PyKCS11
import b

while True:
    time.sleep(0.25)

b.py:

import signal

_op_sigint = signal.getsignal(signal.SIGINT)

def signal_handler(sig, frame):
    print("lib.C_Finalize() here")
    _op_sigint(sig, frame)

signal.signal(signal.SIGINT, signal_handler)

The drawback is that the stacktrace will add upon the users actual position of the Ctrl-C:

anton@bigrigv2 ~ $ python test.py
^CTraceback (most recent call last):
  File "/home/anton/test.py", line 5, in <module>
    time.sleep(0.25)
  File "/home/anton/b.py", line 7, in signal_handler
    _op_sigint(sig, frame)
KeyboardInterrupt

Where it was actually called on time.sleep() not in b.py, but the stack trace will (I hope) stay intact, just having extra entries in the stack. However, this is the smallest and most non-invasive handling I could think of. As it preserves user-set signal handling, as well as defaults to built-in handler of the given signal.

Not sure how you would access the given PyKCS11Lib() instance, to reach self.lib.C_Finalize() tho.

LudovicRousseau commented 4 months ago

I don't think it is a good example code. time.sleep() can be interrupted by Ctrl-C.

In your output I do not see the "lib.C_Finalize() here" message. Is the print() executed?

We need something that is not interruptible in Python.