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
{{ message }}
Repository navigation
Crash on OS X when accessing keyring from multiple processes #281
Description: Crash happens (with -11 exit code) when trying to ask for a password both from the main process and from a subprocess. See the repro case below.
After some digging, it seems that the issue happens in the SecKeychainFindGenericPassword() call of backends/_OS_X_API.py: find_generic_password().
importkeyringfrommultiprocessingimportProcessfrom_address='[email protected]'defmy_subprocess():
keyring.get_password('myapp', from_address)
print('ok')
# comment out the line below to get things work(around)keyring.get_password('myapp', from_address)
proc1=Process(target=my_subprocess)
proc1.start()
proc1.join()
print(proc1.exitcode)
proc2=Process(target=my_subprocess)
proc2.start()
proc2.join()
print(proc2.exitcode)
# expected output:# ok# 0# ok# 0## current output:# -11# -11
Nice work characterizing the issue and providing a repro.
I'm not sure what the next steps are to investigate, so I welcome you or others to continue to pursue a fix. Or I'll surely look into it more as the time permits.
Based on the reproducer you supplied, I've added a test to a branch that replicates the reported failure. I was able to distill the reproducer even more to show that only one subprocess is necessary. I'm fairly sure that what's going on here is multiprocessing is using fork to create the new processes... and something about the way ctypes or keyring's use of ctypes doesn't work well under a forking model.
I thought maybe forcing all the modules including _OS_X_API to reset in the subprocess might workaround the issue, so I applied this patch:
diff --git a/keyring/core.py b/keyring/core.py
index 28e9b63..655a617 100644
--- a/keyring/core.py
+++ b/keyring/core.py
@@ -185,5 +185,16 @@ def _load_keyring_path(config):
pass
+def reset():
+ import importlib
+ for mod in list(sys.modules):
+ if mod.startswith('keyring.backends'):
+ del sys.modules[mod]
+ global backend
+ backend = importlib.reload(backend)
+
+ init_backend()
+
+
# init the _keyring_backend
init_backend()
diff --git a/keyring/tests/test_multiprocess.py b/keyring/tests/test_multiprocess.py
index 9265e3a..584d57e 100644
--- a/keyring/tests/test_multiprocess.py
+++ b/keyring/tests/test_multiprocess.py
@@ -4,6 +4,7 @@ import keyring
def subprocess_get():
+ keyring.core.reset()
keyring.get_password('test_app', 'test_user')
But the error still occurs. I'm now inclined to think the issue may be with the API itself... that it may not allow invocation by a process and its forked child.
I'm out of time to work on this, but I would recommend someone try to put together a reproducer that doesn't use keyring, but simply calls the ctypes calls as found in _OS_X_API.py. If the segfault can be recreated that way, it might implicate fundamental issues in the API. If not, that suggests there's something that keyring could do differently to address the condition.
I've been trying to come up with a keyring-free test case, this is how far I got (nokeyring.py):
importctypesfrommultiprocessingimportProcessdefsec_keychain_find_generic_password(procname):
_sec=ctypes.CDLL('/System/Library/Frameworks/{name}.framework/Versions/A/{name}'.format(name='Security'))
SecKeychainFindGenericPassword=_sec.SecKeychainFindGenericPasswordSecKeychainFindGenericPassword.argtypes= (
ctypes.c_void_p,
ctypes.c_uint32,
ctypes.c_char_p,
ctypes.c_uint32,
ctypes.c_char_p,
ctypes.POINTER(ctypes.c_uint32), # passwordLengthctypes.POINTER(ctypes.c_void_p), # passwordDatactypes.POINTER(ctypes.c_void_p), # itemRef
)
SecKeychainFindGenericPassword.restype=ctypes.c_int32print('before SecKeychainFindGenericPassword in %s'%procname)
status=SecKeychainFindGenericPassword(None, 0, None, 0, None, None, None, None)
print('after SecKeychainFindGenericPassword in %s'%procname)
# comment out the line below to get things work(around)sec_keychain_find_generic_password('main')
proc=Process(target=sec_keychain_find_generic_password, args=('subprocess',))
proc.start()
proc.join()
print('subprocess exit code: %s'%proc.exitcode)
Running this gives:
$ python3 nokeyring.py
before SecKeychainFindGenericPassword in main
after SecKeychainFindGenericPassword in main
before SecKeychainFindGenericPassword in subprocess
subprocess exit code: -11
Commenting out the call in main results in:
$ python3 nokeyring.py #modified
before SecKeychainFindGenericPassword in subprocess
after SecKeychainFindGenericPassword in subprocess
subprocess exit code: 0
Yes, that definitely helps. You've reproduced the issue without keyring. Next will be to figure out if the issue is in Python/ctypes or inherent in the API.
It seems that it is an API problem :/. Here is an Objective-C(-ish?) repro (nokeyring.m).
#import<Security/Security.h>voidsec_keychain_find_generic_password(constchar *procname) {
printf("before SecKeychainFindGenericPassword in %s\n", procname);
SecKeychainFindGenericPassword(NULL, 0, NULL, 0, NULL, NULL, NULL, NULL);
printf("after SecKeychainFindGenericPassword in %s\n", procname);
}
intmain() {
// comment out the line below to get things work(around)sec_keychain_find_generic_password("main");
pid_t childpid = fork();
if (childpid == 0) {
sec_keychain_find_generic_password("child");
exit(0);
}
elseif (childpid < 0) {
printf("fork error\n");
}
else {
int status;
wait(&status);
if (WIFEXITED(status)) {
printf("child exit code: %d\n", WEXITSTATUS(status));
}
elseif (WIFSIGNALED(status)) {
printf("child signalled: %d\n", WTERMSIG(status));
}
else {
printf("unexpected wait status: %d\n", status);
}
}
return0;
}
Build and run:
$ gcc nokeyring.m -o nokeyring -framework security
$ ./nokeyring
before SecKeychainFindGenericPassword in main
after SecKeychainFindGenericPassword in main
before SecKeychainFindGenericPassword in child
child signalled: 11
Build and run with call in main process commented out:
$ ./nokeyring
before SecKeychainFindGenericPassword in child
after SecKeychainFindGenericPassword in child
child exit code: 0
Not sure how to move forward if the native code/API behaves like this.
Aha. Ned Deily pointed me to the macOS man page for fork which states clearly:
There are limits to what you can do in the child process. To be totally safe you should restrict yourself to only executing async-signal safe operations until such time as one of the exec functions is called. All APIs, including global data symbols, in any framework or library should be assumed to be unsafe after a fork() unless explicitly documented to be safe or async-signal safe. If you need to use these frameworks in the child process, you must exec. In this situation it is reasonable to exec yourself.
So that pretty definitively settles it. You can't use keychain after fork.
I do have some good news, however. With Python 3.8, the multiprocessing module prefers exec/spawn for multiprocess.Process, meaning the issue no longer manifest.
I just confirmed that this script executes without error on Python 3.8:
importctypesimportmultiprocessingdefsec_keychain_find_generic_password(procname):
_sec=ctypes.CDLL('/System/Library/Frameworks/{name}.framework/Versions/A/{name}'.format(name='Security'))
SecKeychainFindGenericPassword=_sec.SecKeychainFindGenericPasswordSecKeychainFindGenericPassword.argtypes= (
ctypes.c_void_p,
ctypes.c_uint32,
ctypes.c_char_p,
ctypes.c_uint32,
ctypes.c_char_p,
ctypes.POINTER(ctypes.c_uint32), # passwordLengthctypes.POINTER(ctypes.c_void_p), # passwordDatactypes.POINTER(ctypes.c_void_p), # itemRef
)
SecKeychainFindGenericPassword.restype=ctypes.c_int32print('before SecKeychainFindGenericPassword in %s'%procname)
status=SecKeychainFindGenericPassword(None, 0, None, 0, None, None, None, None)
print('after SecKeychainFindGenericPassword in %s'%procname)
defmain():
# comment out the line below to get things work(around)sec_keychain_find_generic_password('main')
proc=multiprocessing.Process(target=sec_keychain_find_generic_password, args=('subprocess',))
proc.start()
proc.join()
print('subprocess exit code: %s'%proc.exitcode)
__name__=='__main__'andmain()
Activity
renatahodovan commentedon Jun 20, 2017
The crash is also valid on the latest master (3ed3cae).
jaraco commentedon Jun 25, 2017
Nice work characterizing the issue and providing a repro.
I'm not sure what the next steps are to investigate, so I welcome you or others to continue to pursue a fix. Or I'll surely look into it more as the time permits.
Add new test capturing failure. Ref #281.
jaraco commentedon Dec 6, 2018
Based on the reproducer you supplied, I've added a test to a branch that replicates the reported failure. I was able to distill the reproducer even more to show that only one subprocess is necessary. I'm fairly sure that what's going on here is multiprocessing is using
forkto create the new processes... and something about the way ctypes or keyring's use of ctypes doesn't work well under a forking model.jaraco commentedon Dec 6, 2018
This issue/answer seems related. Probably keyring needs to have its OS_X_API module reinitialized in the subprocesses.
jaraco commentedon Dec 6, 2018
When the process crashes, I see this in the "Problem Report" that pops up due to the segmentation fault:
jaraco commentedon Dec 6, 2018
I tried adding this patch, but it did not help:
jaraco commentedon Dec 6, 2018
I thought maybe forcing all the modules including
_OS_X_APIto reset in the subprocess might workaround the issue, so I applied this patch:But the error still occurs. I'm now inclined to think the issue may be with the API itself... that it may not allow invocation by a process and its forked child.
I'm out of time to work on this, but I would recommend someone try to put together a reproducer that doesn't use keyring, but simply calls the ctypes calls as found in _OS_X_API.py. If the segfault can be recreated that way, it might implicate fundamental issues in the API. If not, that suggests there's something that keyring could do differently to address the condition.
akosthekiss commentedon Dec 12, 2018
I've been trying to come up with a keyring-free test case, this is how far I got (nokeyring.py):
Running this gives:
Commenting out the call in main results in:
I hope this helps.
jaraco commentedon Dec 13, 2018
Yes, that definitely helps. You've reproduced the issue without keyring. Next will be to figure out if the issue is in Python/ctypes or inherent in the API.
akosthekiss commentedon Dec 13, 2018
It seems that it is an API problem :/. Here is an Objective-C(-ish?) repro (nokeyring.m).
Build and run:
Build and run with call in main process commented out:
Not sure how to move forward if the native code/API behaves like this.
Workaround for keyring crash on OS X.
bhedavivek commentedon Sep 9, 2019
I think I am facing a similar issue when keyring tries to store a generic password in a subprocess.
fails for me particularly in
keyring.backend._OS_X_APIline 235. Keyring version 13.2.1jaraco commentedon Sep 11, 2019
I've posted this question in the forums, hoping they can shed some light on the issue.
jaraco commentedon Sep 11, 2019
Aha. Ned Deily pointed me to the macOS man page for fork which states clearly:
So that pretty definitively settles it. You can't use keychain after fork.
I do have some good news, however. With Python 3.8, the multiprocessing module prefers exec/spawn for multiprocess.Process, meaning the issue no longer manifest.
I just confirmed that this script executes without error on Python 3.8:
Skip tests in environments known to fail. Ref #281.