Skip to content

Commit 5b4116e

Browse files
grgalexZeroIntensitypicnixzencukou
authored andcommitted
pythongh-126554: ctypes: Correctly handle NULL dlsym values (pythonGH-126555)
For dlsym(), a return value of NULL does not necessarily indicate an error [1]. Therefore, to avoid using stale (or NULL) dlerror() values, we must: 1. clear the previous error state by calling dlerror() 2. call dlsym() 3. call dlerror() If the return value of dlerror() is not NULL, an error occured. In ctypes we choose to treat a NULL return value from dlsym() as a "not found" error. This is the same as the fallback message we use on Windows, Cygwin or when getting/formatting the error reason fails. [1]: https://man7.org/linux/man-pages/man3/dlsym.3.html (cherry picked from commit 8717f79) Co-authored-by: George Alexopoulos <[email protected]> Signed-off-by: Georgios Alexopoulos <[email protected]> Signed-off-by: Georgios Alexopoulos <[email protected]> Co-authored-by: Peter Bierma <[email protected]> Co-authored-by: Bénédikt Tran <[email protected]> Co-authored-by: Petr Viktorin <[email protected]>
1 parent cb07c44 commit 5b4116e

File tree

4 files changed

+220
-31
lines changed

4 files changed

+220
-31
lines changed

Lib/test/test_ctypes/test_dlerror.py

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import os
2+
import sys
3+
import unittest
4+
import platform
5+
6+
FOO_C = r"""
7+
#include <unistd.h>
8+
9+
/* This is a 'GNU indirect function' (IFUNC) that will be called by
10+
dlsym() to resolve the symbol "foo" to an address. Typically, such
11+
a function would return the address of an actual function, but it
12+
can also just return NULL. For some background on IFUNCs, see
13+
https://willnewton.name/uncategorized/using-gnu-indirect-functions.
14+
15+
Adapted from Michael Kerrisk's answer: https://stackoverflow.com/a/53590014.
16+
*/
17+
18+
asm (".type foo STT_GNU_IFUNC");
19+
20+
void *foo(void)
21+
{
22+
write($DESCRIPTOR, "OK", 2);
23+
return NULL;
24+
}
25+
"""
26+
27+
28+
@unittest.skipUnless(sys.platform.startswith('linux'),
29+
'Test only valid for Linux')
30+
class TestNullDlsym(unittest.TestCase):
31+
"""GH-126554: Ensure that we catch NULL dlsym return values
32+
33+
In rare cases, such as when using GNU IFUNCs, dlsym(),
34+
the C function that ctypes' CDLL uses to get the address
35+
of symbols, can return NULL.
36+
37+
The objective way of telling if an error during symbol
38+
lookup happened is to call glibc's dlerror() and check
39+
for a non-NULL return value.
40+
41+
However, there can be cases where dlsym() returns NULL
42+
and dlerror() is also NULL, meaning that glibc did not
43+
encounter any error.
44+
45+
In the case of ctypes, we subjectively treat that as
46+
an error, and throw a relevant exception.
47+
48+
This test case ensures that we correctly enforce
49+
this 'dlsym returned NULL -> throw Error' rule.
50+
"""
51+
52+
def test_null_dlsym(self):
53+
import subprocess
54+
import tempfile
55+
56+
# To avoid ImportErrors on Windows, where _ctypes does not have
57+
# dlopen and dlsym,
58+
# import here, i.e., inside the test function.
59+
# The skipUnless('linux') decorator ensures that we're on linux
60+
# if we're executing these statements.
61+
from ctypes import CDLL, c_int
62+
from _ctypes import dlopen, dlsym
63+
64+
retcode = subprocess.call(["gcc", "--version"],
65+
stdout=subprocess.DEVNULL,
66+
stderr=subprocess.DEVNULL)
67+
if retcode != 0:
68+
self.skipTest("gcc is missing")
69+
70+
pipe_r, pipe_w = os.pipe()
71+
self.addCleanup(os.close, pipe_r)
72+
self.addCleanup(os.close, pipe_w)
73+
74+
with tempfile.TemporaryDirectory() as d:
75+
# Create a C file with a GNU Indirect Function (FOO_C)
76+
# and compile it into a shared library.
77+
srcname = os.path.join(d, 'foo.c')
78+
dstname = os.path.join(d, 'libfoo.so')
79+
with open(srcname, 'w') as f:
80+
f.write(FOO_C.replace('$DESCRIPTOR', str(pipe_w)))
81+
args = ['gcc', '-fPIC', '-shared', '-o', dstname, srcname]
82+
p = subprocess.run(args, capture_output=True)
83+
84+
if p.returncode != 0:
85+
# IFUNC is not supported on all architectures.
86+
if platform.machine() == 'x86_64':
87+
# It should be supported here. Something else went wrong.
88+
p.check_returncode()
89+
else:
90+
# IFUNC might not be supported on this machine.
91+
self.skipTest(f"could not compile indirect function: {p}")
92+
93+
# Case #1: Test 'PyCFuncPtr_FromDll' from Modules/_ctypes/_ctypes.c
94+
L = CDLL(dstname)
95+
with self.assertRaisesRegex(AttributeError, "function 'foo' not found"):
96+
# Try accessing the 'foo' symbol.
97+
# It should resolve via dlsym() to NULL,
98+
# and since we subjectively treat NULL
99+
# addresses as errors, we should get
100+
# an error.
101+
L.foo
102+
103+
# Assert that the IFUNC was called
104+
self.assertEqual(os.read(pipe_r, 2), b'OK')
105+
106+
# Case #2: Test 'CDataType_in_dll_impl' from Modules/_ctypes/_ctypes.c
107+
with self.assertRaisesRegex(ValueError, "symbol 'foo' not found"):
108+
c_int.in_dll(L, "foo")
109+
110+
# Assert that the IFUNC was called
111+
self.assertEqual(os.read(pipe_r, 2), b'OK')
112+
113+
# Case #3: Test 'py_dl_sym' from Modules/_ctypes/callproc.c
114+
L = dlopen(dstname)
115+
with self.assertRaisesRegex(OSError, "symbol 'foo' not found"):
116+
dlsym(L, "foo")
117+
118+
# Assert that the IFUNC was called
119+
self.assertEqual(os.read(pipe_r, 2), b'OK')
120+
121+
122+
if __name__ == "__main__":
123+
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix error handling in :class:`ctypes.CDLL` objects
2+
which could result in a crash in rare situations.

Modules/_ctypes/_ctypes.c

+64-26
Original file line numberDiff line numberDiff line change
@@ -958,32 +958,48 @@ CDataType_in_dll_impl(PyObject *type, PyTypeObject *cls, PyObject *dll,
958958
return NULL;
959959
}
960960

961+
#undef USE_DLERROR
961962
#ifdef MS_WIN32
962963
Py_BEGIN_ALLOW_THREADS
963964
address = (void *)GetProcAddress(handle, name);
964965
Py_END_ALLOW_THREADS
965-
if (!address) {
966-
PyErr_Format(PyExc_ValueError,
967-
"symbol '%s' not found",
968-
name);
969-
return NULL;
970-
}
971966
#else
967+
#ifdef __CYGWIN__
968+
// dlerror() isn't very helpful on cygwin
969+
#else
970+
#define USE_DLERROR
971+
/* dlerror() always returns the latest error.
972+
*
973+
* Clear the previous value before calling dlsym(),
974+
* to ensure we can tell if our call resulted in an error.
975+
*/
976+
(void)dlerror();
977+
#endif
972978
address = (void *)dlsym(handle, name);
973-
if (!address) {
974-
#ifdef __CYGWIN__
975-
/* dlerror() isn't very helpful on cygwin */
976-
PyErr_Format(PyExc_ValueError,
977-
"symbol '%s' not found",
978-
name);
979-
#else
980-
PyErr_SetString(PyExc_ValueError, dlerror());
981979
#endif
982-
return NULL;
980+
981+
if (address) {
982+
ctypes_state *st = get_module_state_by_def(Py_TYPE(type));
983+
return PyCData_AtAddress(st, type, address);
983984
}
984-
#endif
985-
ctypes_state *st = get_module_state_by_def(Py_TYPE(type));
986-
return PyCData_AtAddress(st, type, address);
985+
986+
#ifdef USE_DLERROR
987+
const char *dlerr = dlerror();
988+
if (dlerr) {
989+
PyObject *message = PyUnicode_DecodeLocale(dlerr, "surrogateescape");
990+
if (message) {
991+
PyErr_SetObject(PyExc_ValueError, message);
992+
Py_DECREF(message);
993+
return NULL;
994+
}
995+
// Ignore errors from PyUnicode_DecodeLocale,
996+
// fall back to the generic error below.
997+
PyErr_Clear();
998+
}
999+
#endif
1000+
#undef USE_DLERROR
1001+
PyErr_Format(PyExc_ValueError, "symbol '%s' not found", name);
1002+
return NULL;
9871003
}
9881004

9891005
/*[clinic input]
@@ -3743,6 +3759,7 @@ PyCFuncPtr_FromDll(PyTypeObject *type, PyObject *args, PyObject *kwds)
37433759
return NULL;
37443760
}
37453761

3762+
#undef USE_DLERROR
37463763
#ifdef MS_WIN32
37473764
address = FindAddress(handle, name, (PyObject *)type);
37483765
if (!address) {
@@ -3758,20 +3775,41 @@ PyCFuncPtr_FromDll(PyTypeObject *type, PyObject *args, PyObject *kwds)
37583775
return NULL;
37593776
}
37603777
#else
3778+
#ifdef __CYGWIN__
3779+
//dlerror() isn't very helpful on cygwin */
3780+
#else
3781+
#define USE_DLERROR
3782+
/* dlerror() always returns the latest error.
3783+
*
3784+
* Clear the previous value before calling dlsym(),
3785+
* to ensure we can tell if our call resulted in an error.
3786+
*/
3787+
(void)dlerror();
3788+
#endif
37613789
address = (PPROC)dlsym(handle, name);
3790+
37623791
if (!address) {
3763-
#ifdef __CYGWIN__
3764-
/* dlerror() isn't very helpful on cygwin */
3765-
PyErr_Format(PyExc_AttributeError,
3766-
"function '%s' not found",
3767-
name);
3768-
#else
3769-
PyErr_SetString(PyExc_AttributeError, dlerror());
3770-
#endif
3792+
#ifdef USE_DLERROR
3793+
const char *dlerr = dlerror();
3794+
if (dlerr) {
3795+
PyObject *message = PyUnicode_DecodeLocale(dlerr, "surrogateescape");
3796+
if (message) {
3797+
PyErr_SetObject(PyExc_AttributeError, message);
3798+
Py_DECREF(ftuple);
3799+
Py_DECREF(message);
3800+
return NULL;
3801+
}
3802+
// Ignore errors from PyUnicode_DecodeLocale,
3803+
// fall back to the generic error below.
3804+
PyErr_Clear();
3805+
}
3806+
#endif
3807+
PyErr_Format(PyExc_AttributeError, "function '%s' not found", name);
37713808
Py_DECREF(ftuple);
37723809
return NULL;
37733810
}
37743811
#endif
3812+
#undef USE_DLERROR
37753813
ctypes_state *st = get_module_state_by_def(Py_TYPE(type));
37763814
if (!_validate_paramflags(st, type, paramflags)) {
37773815
Py_DECREF(ftuple);

Modules/_ctypes/callproc.c

+31-5
Original file line numberDiff line numberDiff line change
@@ -1614,13 +1614,39 @@ static PyObject *py_dl_sym(PyObject *self, PyObject *args)
16141614
if (PySys_Audit("ctypes.dlsym/handle", "O", args) < 0) {
16151615
return NULL;
16161616
}
1617+
#undef USE_DLERROR
1618+
#ifdef __CYGWIN__
1619+
// dlerror() isn't very helpful on cygwin
1620+
#else
1621+
#define USE_DLERROR
1622+
/* dlerror() always returns the latest error.
1623+
*
1624+
* Clear the previous value before calling dlsym(),
1625+
* to ensure we can tell if our call resulted in an error.
1626+
*/
1627+
(void)dlerror();
1628+
#endif
16171629
ptr = dlsym((void*)handle, name);
1618-
if (!ptr) {
1619-
PyErr_SetString(PyExc_OSError,
1620-
dlerror());
1621-
return NULL;
1630+
if (ptr) {
1631+
return PyLong_FromVoidPtr(ptr);
1632+
}
1633+
#ifdef USE_DLERROR
1634+
const char *dlerr = dlerror();
1635+
if (dlerr) {
1636+
PyObject *message = PyUnicode_DecodeLocale(dlerr, "surrogateescape");
1637+
if (message) {
1638+
PyErr_SetObject(PyExc_OSError, message);
1639+
Py_DECREF(message);
1640+
return NULL;
1641+
}
1642+
// Ignore errors from PyUnicode_DecodeLocale,
1643+
// fall back to the generic error below.
1644+
PyErr_Clear();
16221645
}
1623-
return PyLong_FromVoidPtr(ptr);
1646+
#endif
1647+
#undef USE_DLERROR
1648+
PyErr_Format(PyExc_OSError, "symbol '%s' not found", name);
1649+
return NULL;
16241650
}
16251651
#endif
16261652

0 commit comments

Comments
 (0)