Skip to content

Commit edd033a

Browse files
picnixzvstinner
andcommitted
pythongh-126742: Add _PyErr_SetLocaleString, use it for gdbm & dlerror messages (pythonGH-126746)
- Add a helper to set an error from locale-encoded `char*` - Use the helper for gdbm & dlerror messages Co-authored-by: Victor Stinner <[email protected]>
1 parent 6ac578c commit edd033a

File tree

12 files changed

+176
-68
lines changed

12 files changed

+176
-68
lines changed

Include/internal/pycore_pyerrors.h

Lines changed: 12 additions & 0 deletions
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

Lines changed: 70 additions & 10 deletions
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

Lines changed: 17 additions & 5 deletions
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

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -803,15 +803,8 @@ CDataType_in_dll(PyObject *type, PyObject *args)
803803
#ifdef USE_DLERROR
804804
const char *dlerr = dlerror();
805805
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();
806+
_PyErr_SetLocaleString(PyExc_ValueError, dlerr);
807+
return NULL;
815808
}
816809
#endif
817810
#undef USE_DLERROR
@@ -3646,21 +3639,14 @@ PyCFuncPtr_FromDll(PyTypeObject *type, PyObject *args, PyObject *kwds)
36463639
#endif
36473640
address = (PPROC)dlsym(handle, name);
36483641
if (!address) {
3649-
#ifdef USE_DLERROR
3642+
#ifdef USE_DLERROR
36503643
const char *dlerr = dlerror();
36513644
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();
3645+
_PyErr_SetLocaleString(PyExc_AttributeError, dlerr);
3646+
Py_DECREF(ftuple);
3647+
return NULL;
36623648
}
3663-
#endif
3649+
#endif
36643650
PyErr_Format(PyExc_AttributeError, "function '%s' not found", name);
36653651
Py_DECREF(ftuple);
36663652
return NULL;

Modules/_ctypes/callproc.c

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1550,10 +1550,11 @@ static PyObject *py_dl_open(PyObject *self, PyObject *args)
15501550
Py_XDECREF(name2);
15511551
if (!handle) {
15521552
const char *errmsg = dlerror();
1553-
if (!errmsg)
1554-
errmsg = "dlopen() error";
1555-
PyErr_SetString(PyExc_OSError,
1556-
errmsg);
1553+
if (errmsg) {
1554+
_PyErr_SetLocaleString(PyExc_OSError, errmsg);
1555+
return NULL;
1556+
}
1557+
PyErr_SetString(PyExc_OSError, "dlopen() error");
15571558
return NULL;
15581559
}
15591560
return PyLong_FromVoidPtr(handle);
@@ -1566,8 +1567,12 @@ static PyObject *py_dl_close(PyObject *self, PyObject *args)
15661567
if (!PyArg_ParseTuple(args, "O&:dlclose", &_parse_voidp, &handle))
15671568
return NULL;
15681569
if (dlclose(handle)) {
1569-
PyErr_SetString(PyExc_OSError,
1570-
dlerror());
1570+
const char *errmsg = dlerror();
1571+
if (errmsg) {
1572+
_PyErr_SetLocaleString(PyExc_OSError, errmsg);
1573+
return NULL;
1574+
}
1575+
PyErr_SetString(PyExc_OSError, "dlclose() error");
15711576
return NULL;
15721577
}
15731578
Py_RETURN_NONE;
@@ -1601,21 +1606,14 @@ static PyObject *py_dl_sym(PyObject *self, PyObject *args)
16011606
if (ptr) {
16021607
return PyLong_FromVoidPtr(ptr);
16031608
}
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();
1609+
#ifdef USE_DLERROR
1610+
const char *errmsg = dlerror();
1611+
if (errmsg) {
1612+
_PyErr_SetLocaleString(PyExc_OSError, errmsg);
1613+
return NULL;
16161614
}
1617-
#endif
1618-
#undef USE_DLERROR
1615+
#endif
1616+
#undef USE_DLERROR
16191617
PyErr_Format(PyExc_OSError, "symbol '%s' not found", name);
16201618
return NULL;
16211619
}

Modules/_gdbmmodule.c

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55

66
#define PY_SSIZE_T_CLEAN
77
#include "Python.h"
8+
#include "pycore_pyerrors.h" // _PyErr_SetLocaleString()
89
#include "gdbm.h"
910

1011
#include <fcntl.h>
11-
#include <stdlib.h> // free()
12+
#include <stdlib.h> // free()
1213
#include <sys/stat.h>
1314
#include <sys/types.h>
1415

@@ -30,6 +31,24 @@ get_gdbm_state(PyObject *module)
3031
return (_gdbm_state *)state;
3132
}
3233

34+
/*
35+
* Set the gdbm error obtained by gdbm_strerror(gdbm_errno).
36+
*
37+
* If no error message exists, a generic (UTF-8) error message
38+
* is used instead.
39+
*/
40+
static void
41+
set_gdbm_error(_gdbm_state *state, const char *generic_error)
42+
{
43+
const char *gdbm_errmsg = gdbm_strerror(gdbm_errno);
44+
if (gdbm_errmsg) {
45+
_PyErr_SetLocaleString(state->gdbm_error, gdbm_errmsg);
46+
}
47+
else {
48+
PyErr_SetString(state->gdbm_error, generic_error);
49+
}
50+
}
51+
3352
/*[clinic input]
3453
module _gdbm
3554
class _gdbm.gdbm "gdbmobject *" "&Gdbmtype"
@@ -88,7 +107,7 @@ newgdbmobject(_gdbm_state *state, const char *file, int flags, int mode)
88107
PyErr_SetFromErrnoWithFilename(state->gdbm_error, file);
89108
}
90109
else {
91-
PyErr_SetString(state->gdbm_error, gdbm_strerror(gdbm_errno));
110+
set_gdbm_error(state, "gdbm_open() error");
92111
}
93112
Py_DECREF(dp);
94113
return NULL;
@@ -133,7 +152,7 @@ gdbm_length(gdbmobject *dp)
133152
PyErr_SetFromErrno(state->gdbm_error);
134153
}
135154
else {
136-
PyErr_SetString(state->gdbm_error, gdbm_strerror(gdbm_errno));
155+
set_gdbm_error(state, "gdbm_count() error");
137156
}
138157
return -1;
139158
}
@@ -283,7 +302,7 @@ gdbm_ass_sub(gdbmobject *dp, PyObject *v, PyObject *w)
283302
PyErr_SetObject(PyExc_KeyError, v);
284303
}
285304
else {
286-
PyErr_SetString(state->gdbm_error, gdbm_strerror(gdbm_errno));
305+
set_gdbm_error(state, "gdbm_delete() error");
287306
}
288307
return -1;
289308
}
@@ -294,11 +313,12 @@ gdbm_ass_sub(gdbmobject *dp, PyObject *v, PyObject *w)
294313
}
295314
errno = 0;
296315
if (gdbm_store(dp->di_dbm, krec, drec, GDBM_REPLACE) < 0) {
297-
if (errno != 0)
316+
if (errno != 0) {
298317
PyErr_SetFromErrno(state->gdbm_error);
299-
else
300-
PyErr_SetString(state->gdbm_error,
301-
gdbm_strerror(gdbm_errno));
318+
}
319+
else {
320+
set_gdbm_error(state, "gdbm_store() error");
321+
}
302322
return -1;
303323
}
304324
}
@@ -531,10 +551,12 @@ _gdbm_gdbm_reorganize_impl(gdbmobject *self, PyTypeObject *cls)
531551
check_gdbmobject_open(self, state->gdbm_error);
532552
errno = 0;
533553
if (gdbm_reorganize(self->di_dbm) < 0) {
534-
if (errno != 0)
554+
if (errno != 0) {
535555
PyErr_SetFromErrno(state->gdbm_error);
536-
else
537-
PyErr_SetString(state->gdbm_error, gdbm_strerror(gdbm_errno));
556+
}
557+
else {
558+
set_gdbm_error(state, "gdbm_reorganize() error");
559+
}
538560
return NULL;
539561
}
540562
Py_RETURN_NONE;

Modules/_hashopenssl.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ _setException(PyObject *exc, const char* altmsg, ...)
320320
va_end(vargs);
321321
ERR_clear_error();
322322

323+
/* ERR_ERROR_STRING(3) ensures that the messages below are ASCII */
323324
lib = ERR_lib_error_string(errcode);
324325
func = ERR_func_error_string(errcode);
325326
reason = ERR_reason_error_string(errcode);

Modules/_sqlite/util.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ _pysqlite_seterror(pysqlite_state *state, sqlite3 *db)
129129

130130
/* Create and set the exception. */
131131
int extended_errcode = sqlite3_extended_errcode(db);
132+
// sqlite3_errmsg() always returns an UTF-8 encoded message
132133
const char *errmsg = sqlite3_errmsg(db);
133134
raise_exception(exc_class, extended_errcode, errmsg);
134135
return extended_errcode;

Modules/_testcapi/exceptions.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#define PY_SSIZE_T_CLEAN
22
#include "parts.h"
33
#include "util.h"
4+
45
#include "clinic/exceptions.c.h"
56

67

0 commit comments

Comments
 (0)