Skip to content

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

Merged
merged 54 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
3c3c29d
ctypes: Correctly handle NULL dlsym values
grgalex Nov 7, 2024
f71abe8
Update Misc/NEWS.d/next/C_API/2024-11-07-20-24-58.gh-issue-126554.ri1…
grgalex Nov 8, 2024
64a7394
Update Modules/_ctypes/_ctypes.c
grgalex Nov 8, 2024
512a084
Update Modules/_ctypes/_ctypes.c
grgalex Nov 8, 2024
cd7d5d7
Add dlerror() comment, handle callproc.c
grgalex Nov 8, 2024
cf39b5b
test: Add ctypes null dlsym case
grgalex Nov 8, 2024
a72ce98
Add comment to test, minor wip-debugging prints
grgalex Nov 8, 2024
c820d16
Remove wip/debug prints
grgalex Nov 8, 2024
6751125
Update Modules/_ctypes/_ctypes.c
grgalex Nov 8, 2024
e1f3b43
Update Modules/_ctypes/callproc.c
grgalex Nov 8, 2024
8f7587d
Update Modules/_ctypes/_ctypes.c
grgalex Nov 8, 2024
2b3a88c
Explicitly fail if not AttributeError raised
grgalex Nov 8, 2024
9a1af29
Tidy up Assertion in test
grgalex Nov 8, 2024
3e02e09
Update Modules/_ctypes/_ctypes.c
grgalex Nov 9, 2024
fbdbc26
Update Modules/_ctypes/_ctypes.c
grgalex Nov 9, 2024
fdf6013
Update Modules/_ctypes/callproc.c
grgalex Nov 9, 2024
3aa29e5
Update Lib/test/test_ctypes/test_dlerror.py
grgalex Nov 11, 2024
0616214
Update Lib/test/test_ctypes/test_dlerror.py
grgalex Nov 11, 2024
6d59ab7
test: Smoothly check for existence of GCC
grgalex Nov 11, 2024
31c621d
test: Clean up libfoo.so compilation command
grgalex Nov 11, 2024
53475a4
Update Lib/test/test_ctypes/test_dlerror.py
grgalex Nov 11, 2024
7fc538e
Update Modules/_ctypes/_ctypes.c
grgalex Nov 11, 2024
e20d97f
Update Modules/_ctypes/_ctypes.c
grgalex Nov 11, 2024
95b072e
Update Modules/_ctypes/_ctypes.c
grgalex Nov 11, 2024
048385c
Update Modules/_ctypes/callproc.c
grgalex Nov 11, 2024
658098f
Update Modules/_ctypes/callproc.c
grgalex Nov 11, 2024
6f4b921
Update Modules/_ctypes/_ctypes.c
grgalex Nov 11, 2024
25b6cf9
Update Misc/NEWS.d/next/C_API/2024-11-07-20-24-58.gh-issue-126554.ri1…
grgalex Nov 11, 2024
4bac254
test: Clarify string vs list in subprocess.run
grgalex Nov 11, 2024
ee6122c
test: Use string in gcc --version
grgalex Nov 11, 2024
08a7b88
Update Lib/test/test_ctypes/test_dlerror.py
grgalex Nov 11, 2024
8c6b8ae
Write to a pipe instead of stderr
encukou Nov 12, 2024
291bf16
Reorganize dlerror() code
encukou Nov 12, 2024
1cbffcc
Use PyUnicode_DecodeLocale for the error
encukou Nov 12, 2024
5748c13
Clean up PyCFuncPtr_FromDll, uses #define USE_DLERROR
grgalex Nov 12, 2024
65e2b7e
Clean up py_dl_sym, uses #define USE_DLERROR
grgalex Nov 12, 2024
38fbaed
test: Exercise 3 code paths, one for each C function we want to test
grgalex Nov 12, 2024
cc8e366
Add empty statement after label, placate C compiler
grgalex Nov 12, 2024
4319332
Move label to before #endif (WIN32)
grgalex Nov 12, 2024
8356cf4
Import ctypes, _ctypes inside test, so we don't get ImportError in wi…
grgalex Nov 12, 2024
1e1e78b
Revert "Import ctypes, _ctypes inside test, so we don't get ImportErr…
grgalex Nov 12, 2024
4e87040
Remove unnecessary goto, fix preprocessor cmd indentation
grgalex Nov 12, 2024
6a3aea7
test: Remove shell=True from gcc check, use separate arguments
grgalex Nov 12, 2024
73918bf
Remove extra whitespace
grgalex Nov 12, 2024
0ca32d4
test: Move imports to func, avoid ImportError on Windows
grgalex Nov 12, 2024
6db5963
test: Explain why we moved imports to within test function
grgalex Nov 12, 2024
d179377
dlerror: Use surrogateescape error strategy for decoding locale
grgalex Nov 12, 2024
6933314
Update Modules/_ctypes/callproc.c
grgalex Nov 13, 2024
33cf8b7
Clear DecodeLocale error, indent #ifdef, #undef
grgalex Nov 13, 2024
94dfbaa
Decref the message
encukou Nov 13, 2024
cda7771
Update Lib/test/test_ctypes/test_dlerror.py
grgalex Nov 14, 2024
4b1bb47
test: Handle different architectures, where IFUNC not supported
grgalex Nov 14, 2024
2290aa4
Update Modules/_ctypes/callproc.c
grgalex Nov 14, 2024
ab22349
Don't break when under 80 chars
grgalex Nov 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions Lib/test/test_ctypes/test_dlerror.py
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.
"""

def test_null_dlsym(self):
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

# 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.
90 changes: 64 additions & 26 deletions Modules/_ctypes/_ctypes.c
Original file line number Diff line number Diff line change
Expand Up @@ -956,32 +956,48 @@ CDataType_in_dll_impl(PyObject *type, PyTypeObject *cls, PyObject *dll,
return NULL;
}

#undef USE_DLERROR
#ifdef MS_WIN32
Py_BEGIN_ALLOW_THREADS
address = (void *)GetProcAddress(handle, name);
Py_END_ALLOW_THREADS
if (!address) {
PyErr_Format(PyExc_ValueError,
"symbol '%s' not found",
name);
return NULL;
}
#else
#ifdef __CYGWIN__
// 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
address = (void *)dlsym(handle, name);
if (!address) {
#ifdef __CYGWIN__
/* dlerror() isn't very helpful on cygwin */
PyErr_Format(PyExc_ValueError,
"symbol '%s' not found",
name);
#else
PyErr_SetString(PyExc_ValueError, dlerror());
#endif
return NULL;

if (address) {
ctypes_state *st = get_module_state_by_def(Py_TYPE(type));
return PyCData_AtAddress(st, type, address);
}
#endif
ctypes_state *st = get_module_state_by_def(Py_TYPE(type));
return PyCData_AtAddress(st, type, address);

#ifdef USE_DLERROR
const char *dlerr = dlerror();
if (dlerr) {
PyObject *message = PyUnicode_DecodeLocale(dlerr, "surrogateescape");
if (message) {
PyErr_SetObject(PyExc_ValueError, message);
Py_DECREF(message);
return NULL;
}
// Ignore errors from PyUnicode_DecodeLocale,
// fall back to the generic error below.
PyErr_Clear();
}
#endif
#undef USE_DLERROR
PyErr_Format(PyExc_ValueError, "symbol '%s' not found", name);
return NULL;
}

/*[clinic input]
Expand Down Expand Up @@ -3759,6 +3775,7 @@ PyCFuncPtr_FromDll(PyTypeObject *type, PyObject *args, PyObject *kwds)
return NULL;
}

#undef USE_DLERROR
#ifdef MS_WIN32
address = FindAddress(handle, name, (PyObject *)type);
if (!address) {
Expand All @@ -3774,20 +3791,41 @@ PyCFuncPtr_FromDll(PyTypeObject *type, PyObject *args, PyObject *kwds)
return NULL;
}
#else
#ifdef __CYGWIN__
//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
address = (PPROC)dlsym(handle, name);

if (!address) {
#ifdef __CYGWIN__
/* dlerror() isn't very helpful on cygwin */
PyErr_Format(PyExc_AttributeError,
"function '%s' not found",
name);
#else
PyErr_SetString(PyExc_AttributeError, dlerror());
#endif
#ifdef USE_DLERROR
const char *dlerr = dlerror();
if (dlerr) {
PyObject *message = PyUnicode_DecodeLocale(dlerr, "surrogateescape");
if (message) {
PyErr_SetObject(PyExc_AttributeError, message);
Py_DECREF(ftuple);
Py_DECREF(message);
return NULL;
}
// Ignore errors from PyUnicode_DecodeLocale,
// fall back to the generic error below.
PyErr_Clear();
}
#endif
PyErr_Format(PyExc_AttributeError, "function '%s' not found", name);
Py_DECREF(ftuple);
return NULL;
}
#endif
#undef USE_DLERROR
ctypes_state *st = get_module_state_by_def(Py_TYPE(type));
if (!_validate_paramflags(st, type, paramflags)) {
Py_DECREF(ftuple);
Expand Down
36 changes: 31 additions & 5 deletions Modules/_ctypes/callproc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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__
// 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

Expand Down
Loading