Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 34k
gh-126554: ctypes: Correctly handle NULL dlsym values#126555
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
3c3c29df71abe864a7394512a084cd7d5d7cf39b5ba72ce98c820d166751125e1f3b438f7587d2b3a88c9a1af293e02e09fbdbc26fdf60133aa29e506162146d59ab731c621d53475a47fc538ee20d97f95b072e048385c658098f6f4b92125b6cf94bac254ee6122c08a7b888c6b8ae291bf161cbffcc5748c1365e2b7e38fbaedcc8e36643193328356cf41e1e78b4e870406a3aea773918bf0ca32d46db5963d179377693331433cf8b794dfbaacda77714b1bb472290aa4ab22349File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| import os | ||
| import sys | ||
| import unittest | ||
| import platform | ||
| FOO_C = r""" | ||
| #include <unistd.h> | ||
| /* This is a 'GNU indirect function' (IFUNC) that will be called by | ||
| dlsym() to resolve the symbol "foo" to an address. Typically, such | ||
| a function would return the address of an actual function, but it | ||
| can also just return NULL. For some background on IFUNCs, see | ||
| https://willnewton.name/uncategorized/using-gnu-indirect-functions. | ||
| Adapted from Michael Kerrisk's answer: https://stackoverflow.com/a/53590014. | ||
| */ | ||
| asm (".type foo STT_GNU_IFUNC"); | ||
| void *foo(void) | ||
| { | ||
| write($DESCRIPTOR, "OK", 2); | ||
| return NULL; | ||
| } | ||
| """ | ||
| @unittest.skipUnless(sys.platform.startswith('linux'), | ||
| 'Test only valid for Linux') | ||
| class TestNullDlsym(unittest.TestCase): | ||
| """GH-126554: Ensure that we catch NULL dlsym return values | ||
| In rare cases, such as when using GNU IFUNCs, dlsym(), | ||
| the C function that ctypes' CDLL uses to get the address | ||
| of symbols, can return NULL. | ||
| The objective way of telling if an error during symbol | ||
| lookup happened is to call glibc's dlerror() and check | ||
| for a non-NULL return value. | ||
| However, there can be cases where dlsym() returns NULL | ||
| and dlerror() is also NULL, meaning that glibc did not | ||
| encounter any error. | ||
| In the case of ctypes, we subjectively treat that as | ||
| an error, and throw a relevant exception. | ||
| This test case ensures that we correctly enforce | ||
| this 'dlsym returned NULL -> throw Error' rule. | ||
picnixz marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading. Please reload this page. | ||
| """ | ||
| def test_null_dlsym(self): | ||
ZeroIntensity marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading. Please reload this page. | ||
| import subprocess | ||
| import tempfile | ||
| # To avoid ImportErrors on Windows, where _ctypes does not have | ||
| # dlopen and dlsym, | ||
| # import here, i.e., inside the test function. | ||
| # The skipUnless('linux') decorator ensures that we're on linux | ||
| # if we're executing these statements. | ||
| from ctypes import CDLL, c_int | ||
| from _ctypes import dlopen, dlsym | ||
| retcode = subprocess.call(["gcc", "--version"], | ||
| stdout=subprocess.DEVNULL, | ||
| stderr=subprocess.DEVNULL) | ||
| if retcode != 0: | ||
| self.skipTest("gcc is missing") | ||
| pipe_r, pipe_w = os.pipe() | ||
| self.addCleanup(os.close, pipe_r) | ||
| self.addCleanup(os.close, pipe_w) | ||
| with tempfile.TemporaryDirectory() as d: | ||
| # Create a C file with a GNU Indirect Function (FOO_C) | ||
| # and compile it into a shared library. | ||
| srcname = os.path.join(d, 'foo.c') | ||
| dstname = os.path.join(d, 'libfoo.so') | ||
| with open(srcname, 'w') as f: | ||
| f.write(FOO_C.replace('$DESCRIPTOR', str(pipe_w))) | ||
| args = ['gcc', '-fPIC', '-shared', '-o', dstname, srcname] | ||
| p = subprocess.run(args, capture_output=True) | ||
| if p.returncode != 0: | ||
| # IFUNC is not supported on all architectures. | ||
| if platform.machine() == 'x86_64': | ||
| # It should be supported here. Something else went wrong. | ||
| p.check_returncode() | ||
| else: | ||
| # IFUNC might not be supported on this machine. | ||
| self.skipTest(f"could not compile indirect function:{p}") | ||
| # Case #1: Test 'PyCFuncPtr_FromDll' from Modules/_ctypes/_ctypes.c | ||
| L = CDLL(dstname) | ||
| with self.assertRaisesRegex(AttributeError, "function 'foo' not found"): | ||
| # Try accessing the 'foo' symbol. | ||
| # It should resolve via dlsym() to NULL, | ||
| # and since we subjectively treat NULL | ||
| # addresses as errors, we should get | ||
| # an error. | ||
| L.foo | ||
ZeroIntensity marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading. Please reload this page. | ||
| # Assert that the IFUNC was called | ||
| self.assertEqual(os.read(pipe_r, 2), b'OK') | ||
| # Case #2: Test 'CDataType_in_dll_impl' from Modules/_ctypes/_ctypes.c | ||
| with self.assertRaisesRegex(ValueError, "symbol 'foo' not found"): | ||
| c_int.in_dll(L, "foo") | ||
| # Assert that the IFUNC was called | ||
| self.assertEqual(os.read(pipe_r, 2), b'OK') | ||
| # Case #3: Test 'py_dl_sym' from Modules/_ctypes/callproc.c | ||
| L = dlopen(dstname) | ||
| with self.assertRaisesRegex(OSError, "symbol 'foo' not found"): | ||
| dlsym(L, "foo") | ||
| # Assert that the IFUNC was called | ||
| self.assertEqual(os.read(pipe_r, 2), b'OK') | ||
| if __name__ == "__main__": | ||
| unittest.main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| Fix error handling in :class:`ctypes.CDLL` objects | ||
| which could result in a crash in rare situations. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1623,13 +1623,39 @@ static PyObject *py_dl_sym(PyObject *self, PyObject *args) | ||
| if (PySys_Audit("ctypes.dlsym/handle", "O", args) < 0){ | ||
| return NULL; | ||
| } | ||
| #undef USE_DLERROR | ||
| #ifdef __CYGWIN__ | ||
grgalex marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading. Please reload this page. | ||
| // dlerror() isn't very helpful on cygwin | ||
| #else | ||
| #define USE_DLERROR | ||
| /* dlerror() always returns the latest error. | ||
| * | ||
| * Clear the previous value before calling dlsym(), | ||
| * to ensure we can tell if our call resulted in an error. | ||
| */ | ||
| (void)dlerror(); | ||
| #endif | ||
| ptr = dlsym((void*)handle, name); | ||
| if (!ptr){ | ||
| PyErr_SetString(PyExc_OSError, | ||
| dlerror()); | ||
| return NULL; | ||
| if (ptr){ | ||
| return PyLong_FromVoidPtr(ptr); | ||
| } | ||
| #ifdef USE_DLERROR | ||
| const char *dlerr = dlerror(); | ||
| if (dlerr){ | ||
| PyObject *message = PyUnicode_DecodeLocale(dlerr, "surrogateescape"); | ||
| if (message){ | ||
| PyErr_SetObject(PyExc_OSError, message); | ||
| Py_DECREF(message); | ||
| return NULL; | ||
| } | ||
| // Ignore errors from PyUnicode_DecodeLocale, | ||
| // fall back to the generic error below. | ||
| PyErr_Clear(); | ||
| } | ||
| return PyLong_FromVoidPtr(ptr); | ||
| #endif | ||
| #undef USE_DLERROR | ||
| PyErr_Format(PyExc_OSError, "symbol '%s' not found", name); | ||
| return NULL; | ||
| } | ||
| #endif | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.