Skip to content

Commit e0b61ff

Browse files
picnixzvstinner
andauthored
[3.12] gh-126742: Add _PyErr_SetLocaleString, use it for gdbm & dlerror messages (GH-126746) (GH-128027)
- Add a helper to set an error from locale-encoded `char*` - Use the helper for gdbm & dlerror messages Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent 6ac578c commit e0b61ff

File tree

12 files changed

+183
-68
lines changed

12 files changed

+183
-68
lines changed

Include/internal/pycore_pyerrors.h

+12
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,18 @@ PyAPI_FUNC(void) _PyErr_SetString(
7575
PyObject *exception,
7676
const char *string);
7777

78+
/*
79+
* Set an exception with the error message decoded from the current locale
80+
* encoding (LC_CTYPE).
81+
*
82+
* Exceptions occurring in decoding take priority over the desired exception.
83+
*
84+
* Exported for '_ctypes' shared extensions.
85+
*/
86+
PyAPI_FUNC(void) _PyErr_SetLocaleString(
87+
PyObject *exception,
88+
const char *string);
89+
7890
PyAPI_FUNC(PyObject *) _PyErr_Format(
7991
PyThreadState *tstate,
8092
PyObject *exception,

Lib/test/test_ctypes/test_dlerror.py

+70-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import _ctypes
12
import os
3+
import platform
24
import sys
5+
import test.support
36
import unittest
4-
import platform
7+
from ctypes import CDLL, c_int
8+
from ctypes.util import find_library
9+
510

611
FOO_C = r"""
712
#include <unistd.h>
@@ -26,7 +31,7 @@
2631

2732

2833
@unittest.skipUnless(sys.platform.startswith('linux'),
29-
'Test only valid for Linux')
34+
'test requires GNU IFUNC support')
3035
class TestNullDlsym(unittest.TestCase):
3136
"""GH-126554: Ensure that we catch NULL dlsym return values
3237
@@ -53,14 +58,6 @@ def test_null_dlsym(self):
5358
import subprocess
5459
import tempfile
5560

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-
6461
retcode = subprocess.call(["gcc", "--version"],
6562
stdout=subprocess.DEVNULL,
6663
stderr=subprocess.DEVNULL)
@@ -111,6 +108,8 @@ def test_null_dlsym(self):
111108
self.assertEqual(os.read(pipe_r, 2), b'OK')
112109

113110
# Case #3: Test 'py_dl_sym' from Modules/_ctypes/callproc.c
111+
dlopen = test.support.get_attribute(_ctypes, 'dlopen')
112+
dlsym = test.support.get_attribute(_ctypes, 'dlsym')
114113
L = dlopen(dstname)
115114
with self.assertRaisesRegex(OSError, "symbol 'foo' not found"):
116115
dlsym(L, "foo")
@@ -119,5 +118,66 @@ def test_null_dlsym(self):
119118
self.assertEqual(os.read(pipe_r, 2), b'OK')
120119

121120

121+
@unittest.skipUnless(os.name != 'nt', 'test requires dlerror() calls')
122+
class TestLocalization(unittest.TestCase):
123+
124+
@staticmethod
125+
def configure_locales(func):
126+
return test.support.run_with_locale(
127+
'LC_ALL',
128+
'fr_FR.iso88591', 'ja_JP.sjis', 'zh_CN.gbk',
129+
'fr_FR.utf8', 'en_US.utf8',
130+
'',
131+
)(func)
132+
133+
@classmethod
134+
def setUpClass(cls):
135+
cls.libc_filename = find_library("c")
136+
137+
@configure_locales
138+
def test_localized_error_from_dll(self):
139+
dll = CDLL(self.libc_filename)
140+
with self.assertRaises(AttributeError) as cm:
141+
dll.this_name_does_not_exist
142+
if sys.platform.startswith('linux'):
143+
# On macOS, the filename is not reported by dlerror().
144+
self.assertIn(self.libc_filename, str(cm.exception))
145+
146+
@configure_locales
147+
def test_localized_error_in_dll(self):
148+
dll = CDLL(self.libc_filename)
149+
with self.assertRaises(ValueError) as cm:
150+
c_int.in_dll(dll, 'this_name_does_not_exist')
151+
if sys.platform.startswith('linux'):
152+
# On macOS, the filename is not reported by dlerror().
153+
self.assertIn(self.libc_filename, str(cm.exception))
154+
155+
@unittest.skipUnless(hasattr(_ctypes, 'dlopen'),
156+
'test requires _ctypes.dlopen()')
157+
@configure_locales
158+
def test_localized_error_dlopen(self):
159+
missing_filename = b'missing\xff.so'
160+
# Depending whether the locale, we may encode '\xff' differently
161+
# but we are only interested in avoiding a UnicodeDecodeError
162+
# when reporting the dlerror() error message which contains
163+
# the localized filename.
164+
filename_pattern = r'missing.*?\.so'
165+
with self.assertRaisesRegex(OSError, filename_pattern):
166+
_ctypes.dlopen(missing_filename, 2)
167+
168+
@unittest.skipUnless(hasattr(_ctypes, 'dlopen'),
169+
'test requires _ctypes.dlopen()')
170+
@unittest.skipUnless(hasattr(_ctypes, 'dlsym'),
171+
'test requires _ctypes.dlsym()')
172+
@configure_locales
173+
def test_localized_error_dlsym(self):
174+
dll = _ctypes.dlopen(self.libc_filename)
175+
with self.assertRaises(OSError) as cm:
176+
_ctypes.dlsym(dll, 'this_name_does_not_exist')
177+
if sys.platform.startswith('linux'):
178+
# On macOS, the filename is not reported by dlerror().
179+
self.assertIn(self.libc_filename, str(cm.exception))
180+
181+
122182
if __name__ == "__main__":
123183
unittest.main()

Lib/test/test_dbm_gnu.py

+17-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
from test import support
2-
from test.support import import_helper, cpython_only
3-
gdbm = import_helper.import_module("dbm.gnu") #skip if not supported
4-
import unittest
51
import os
6-
from test.support.os_helper import TESTFN, TESTFN_NONASCII, unlink, FakePath
2+
import unittest
3+
from test import support
4+
from test.support import cpython_only, import_helper
5+
from test.support.os_helper import (TESTFN, TESTFN_NONASCII, FakePath,
6+
create_empty_file, temp_dir, unlink)
77

8+
gdbm = import_helper.import_module("dbm.gnu") # skip if not supported
89

910
filename = TESTFN
1011

@@ -192,6 +193,17 @@ def test_open_with_bytes_path(self):
192193
def test_open_with_pathlib_bytes_path(self):
193194
gdbm.open(FakePath(os.fsencode(filename)), "c").close()
194195

196+
@support.run_with_locale(
197+
'LC_ALL',
198+
'fr_FR.iso88591', 'ja_JP.sjis', 'zh_CN.gbk',
199+
'fr_FR.utf8', 'en_US.utf8',
200+
'',
201+
)
202+
def test_localized_error(self):
203+
with temp_dir() as d:
204+
create_empty_file(os.path.join(d, 'test'))
205+
self.assertRaises(gdbm.error, gdbm.open, filename, 'r')
206+
195207

196208
if __name__ == '__main__':
197209
unittest.main()

Modules/_ctypes/_ctypes.c

+8-21
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ bytes(cdata)
125125
#include "ctypes.h"
126126

127127
#include "pycore_long.h" // _PyLong_GetZero()
128+
#include "pycore_pyerrors.h" // _PyErr_SetLocaleString()
128129

129130
ctypes_state global_state;
130131

@@ -803,15 +804,8 @@ CDataType_in_dll(PyObject *type, PyObject *args)
803804
#ifdef USE_DLERROR
804805
const char *dlerr = dlerror();
805806
if (dlerr) {
806-
PyObject *message = PyUnicode_DecodeLocale(dlerr, "surrogateescape");
807-
if (message) {
808-
PyErr_SetObject(PyExc_ValueError, message);
809-
Py_DECREF(message);
810-
return NULL;
811-
}
812-
// Ignore errors from PyUnicode_DecodeLocale,
813-
// fall back to the generic error below.
814-
PyErr_Clear();
807+
_PyErr_SetLocaleString(PyExc_ValueError, dlerr);
808+
return NULL;
815809
}
816810
#endif
817811
#undef USE_DLERROR
@@ -3646,21 +3640,14 @@ PyCFuncPtr_FromDll(PyTypeObject *type, PyObject *args, PyObject *kwds)
36463640
#endif
36473641
address = (PPROC)dlsym(handle, name);
36483642
if (!address) {
3649-
#ifdef USE_DLERROR
3643+
#ifdef USE_DLERROR
36503644
const char *dlerr = dlerror();
36513645
if (dlerr) {
3652-
PyObject *message = PyUnicode_DecodeLocale(dlerr, "surrogateescape");
3653-
if (message) {
3654-
PyErr_SetObject(PyExc_AttributeError, message);
3655-
Py_DECREF(ftuple);
3656-
Py_DECREF(message);
3657-
return NULL;
3658-
}
3659-
// Ignore errors from PyUnicode_DecodeLocale,
3660-
// fall back to the generic error below.
3661-
PyErr_Clear();
3646+
_PyErr_SetLocaleString(PyExc_AttributeError, dlerr);
3647+
Py_DECREF(ftuple);
3648+
return NULL;
36623649
}
3663-
#endif
3650+
#endif
36643651
PyErr_Format(PyExc_AttributeError, "function '%s' not found", name);
36653652
Py_DECREF(ftuple);
36663653
return NULL;

Modules/_ctypes/callproc.c

+19-20
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@
9696
#define DONT_USE_SEH
9797
#endif
9898

99+
#include "pycore_pyerrors.h" // _PyErr_SetLocaleString()
99100
#include "pycore_runtime.h" // _PyRuntime
100101
#include "pycore_global_objects.h" // _Py_ID()
101102

@@ -1550,10 +1551,11 @@ static PyObject *py_dl_open(PyObject *self, PyObject *args)
15501551
Py_XDECREF(name2);
15511552
if (!handle) {
15521553
const char *errmsg = dlerror();
1553-
if (!errmsg)
1554-
errmsg = "dlopen() error";
1555-
PyErr_SetString(PyExc_OSError,
1556-
errmsg);
1554+
if (errmsg) {
1555+
_PyErr_SetLocaleString(PyExc_OSError, errmsg);
1556+
return NULL;
1557+
}
1558+
PyErr_SetString(PyExc_OSError, "dlopen() error");
15571559
return NULL;
15581560
}
15591561
return PyLong_FromVoidPtr(handle);
@@ -1566,8 +1568,12 @@ static PyObject *py_dl_close(PyObject *self, PyObject *args)
15661568
if (!PyArg_ParseTuple(args, "O&:dlclose", &_parse_voidp, &handle))
15671569
return NULL;
15681570
if (dlclose(handle)) {
1569-
PyErr_SetString(PyExc_OSError,
1570-
dlerror());
1571+
const char *errmsg = dlerror();
1572+
if (errmsg) {
1573+
_PyErr_SetLocaleString(PyExc_OSError, errmsg);
1574+
return NULL;
1575+
}
1576+
PyErr_SetString(PyExc_OSError, "dlclose() error");
15711577
return NULL;
15721578
}
15731579
Py_RETURN_NONE;
@@ -1601,21 +1607,14 @@ static PyObject *py_dl_sym(PyObject *self, PyObject *args)
16011607
if (ptr) {
16021608
return PyLong_FromVoidPtr(ptr);
16031609
}
1604-
#ifdef USE_DLERROR
1605-
const char *dlerr = dlerror();
1606-
if (dlerr) {
1607-
PyObject *message = PyUnicode_DecodeLocale(dlerr, "surrogateescape");
1608-
if (message) {
1609-
PyErr_SetObject(PyExc_OSError, message);
1610-
Py_DECREF(message);
1611-
return NULL;
1612-
}
1613-
// Ignore errors from PyUnicode_DecodeLocale,
1614-
// fall back to the generic error below.
1615-
PyErr_Clear();
1610+
#ifdef USE_DLERROR
1611+
const char *errmsg = dlerror();
1612+
if (errmsg) {
1613+
_PyErr_SetLocaleString(PyExc_OSError, errmsg);
1614+
return NULL;
16161615
}
1617-
#endif
1618-
#undef USE_DLERROR
1616+
#endif
1617+
#undef USE_DLERROR
16191618
PyErr_Format(PyExc_OSError, "symbol '%s' not found", name);
16201619
return NULL;
16211620
}

Modules/_gdbmmodule.c

+38-11
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@
33
/* Author: Anthony Baxter, after dbmmodule.c */
44
/* Doc strings: Mitch Chapman */
55

6+
// required for pycore_pyerrors.h
7+
#ifndef Py_BUILD_CORE_BUILTIN
8+
# define Py_BUILD_CORE_MODULE 1
9+
#endif
10+
611
#define PY_SSIZE_T_CLEAN
712
#include "Python.h"
13+
#include "pycore_pyerrors.h" // _PyErr_SetLocaleString()
814
#include "gdbm.h"
915

1016
#include <fcntl.h>
11-
#include <stdlib.h> // free()
17+
#include <stdlib.h> // free()
1218
#include <sys/stat.h>
1319
#include <sys/types.h>
1420

@@ -30,6 +36,24 @@ get_gdbm_state(PyObject *module)
3036
return (_gdbm_state *)state;
3137
}
3238

39+
/*
40+
* Set the gdbm error obtained by gdbm_strerror(gdbm_errno).
41+
*
42+
* If no error message exists, a generic (UTF-8) error message
43+
* is used instead.
44+
*/
45+
static void
46+
set_gdbm_error(_gdbm_state *state, const char *generic_error)
47+
{
48+
const char *gdbm_errmsg = gdbm_strerror(gdbm_errno);
49+
if (gdbm_errmsg) {
50+
_PyErr_SetLocaleString(state->gdbm_error, gdbm_errmsg);
51+
}
52+
else {
53+
PyErr_SetString(state->gdbm_error, generic_error);
54+
}
55+
}
56+
3357
/*[clinic input]
3458
module _gdbm
3559
class _gdbm.gdbm "gdbmobject *" "&Gdbmtype"
@@ -88,7 +112,7 @@ newgdbmobject(_gdbm_state *state, const char *file, int flags, int mode)
88112
PyErr_SetFromErrnoWithFilename(state->gdbm_error, file);
89113
}
90114
else {
91-
PyErr_SetString(state->gdbm_error, gdbm_strerror(gdbm_errno));
115+
set_gdbm_error(state, "gdbm_open() error");
92116
}
93117
Py_DECREF(dp);
94118
return NULL;
@@ -133,7 +157,7 @@ gdbm_length(gdbmobject *dp)
133157
PyErr_SetFromErrno(state->gdbm_error);
134158
}
135159
else {
136-
PyErr_SetString(state->gdbm_error, gdbm_strerror(gdbm_errno));
160+
set_gdbm_error(state, "gdbm_count() error");
137161
}
138162
return -1;
139163
}
@@ -283,7 +307,7 @@ gdbm_ass_sub(gdbmobject *dp, PyObject *v, PyObject *w)
283307
PyErr_SetObject(PyExc_KeyError, v);
284308
}
285309
else {
286-
PyErr_SetString(state->gdbm_error, gdbm_strerror(gdbm_errno));
310+
set_gdbm_error(state, "gdbm_delete() error");
287311
}
288312
return -1;
289313
}
@@ -294,11 +318,12 @@ gdbm_ass_sub(gdbmobject *dp, PyObject *v, PyObject *w)
294318
}
295319
errno = 0;
296320
if (gdbm_store(dp->di_dbm, krec, drec, GDBM_REPLACE) < 0) {
297-
if (errno != 0)
321+
if (errno != 0) {
298322
PyErr_SetFromErrno(state->gdbm_error);
299-
else
300-
PyErr_SetString(state->gdbm_error,
301-
gdbm_strerror(gdbm_errno));
323+
}
324+
else {
325+
set_gdbm_error(state, "gdbm_store() error");
326+
}
302327
return -1;
303328
}
304329
}
@@ -531,10 +556,12 @@ _gdbm_gdbm_reorganize_impl(gdbmobject *self, PyTypeObject *cls)
531556
check_gdbmobject_open(self, state->gdbm_error);
532557
errno = 0;
533558
if (gdbm_reorganize(self->di_dbm) < 0) {
534-
if (errno != 0)
559+
if (errno != 0) {
535560
PyErr_SetFromErrno(state->gdbm_error);
536-
else
537-
PyErr_SetString(state->gdbm_error, gdbm_strerror(gdbm_errno));
561+
}
562+
else {
563+
set_gdbm_error(state, "gdbm_reorganize() error");
564+
}
538565
return NULL;
539566
}
540567
Py_RETURN_NONE;

0 commit comments

Comments
 (0)