Skip to content

Commit 6863f25

Browse files
committed
fix: traceback file maybe use local import for trace print
Signed-off-by: yihong0618 <[email protected]>
1 parent ffaec6e commit 6863f25

File tree

5 files changed

+113
-31
lines changed

5 files changed

+113
-31
lines changed

Include/internal/pycore_traceback.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ PyAPI_FUNC(int) _Py_DisplaySourceLine(PyObject *, PyObject *, int, int, int *, P
1414
// Export for 'pyexact' shared extension
1515
PyAPI_FUNC(void) _PyTraceback_Add(const char *, const char *, int);
1616

17+
// Helper function to check if it's safe to import traceback module
18+
// Returns 0 if a traceback.py file exists in sys.path[0], 1 if safe
19+
extern int _PyTraceback_IsSafeToImport(void);
20+
1721
/* Write the Python traceback into the file 'fd'. For example:
1822
1923
Traceback (most recent call first):

Lib/test/test_traceback.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5175,6 +5175,43 @@ def expected(t, m, fn, l, f, E, e, z):
51755175
f' +------------------------------------',
51765176
]
51775177
self.assertEqual(actual, expected(**colors))
5178+
class TestTracebackSafety(unittest.TestCase):
5179+
"""Test security fixes for traceback module shadowing (issue gh-138170)"""
5180+
5181+
def setUp(self):
5182+
self.tempdir = tempfile.mkdtemp()
5183+
self.addCleanup(shutil.rmtree, self.tempdir)
5184+
5185+
def test_traceback_shadowing_protection(self):
5186+
"""Test that traceback.py files are not executed during exception handling"""
5187+
import os
5188+
import sys
5189+
import subprocess
5190+
malicious_traceback = os.path.join(self.tempdir, 'traceback.py')
5191+
with open(malicious_traceback, 'w') as f:
5192+
f.write('''
5193+
print("MALICIOUS CODE EXECUTED - SECURITY BREACH!")
5194+
print("This should not appear in traceback output")
5195+
''')
5196+
test_script = os.path.join(self.tempdir, 'test_exception.py')
5197+
with open(test_script, 'w') as f:
5198+
f.write('''
5199+
import sys
5200+
sys.path.insert(0, ".")
5201+
raise ValueError("test exception")
5202+
''')
5203+
old_cwd = os.getcwd()
5204+
try:
5205+
os.chdir(self.tempdir)
5206+
result = subprocess.run([sys.executable, test_script],
5207+
capture_output=True, text=True)
5208+
self.assertIn("ValueError: test exception", result.stderr)
5209+
self.assertNotIn("MALICIOUS CODE EXECUTED", result.stdout)
5210+
self.assertNotIn("MALICIOUS CODE EXECUTED", result.stderr)
5211+
self.assertNotIn("This should not appear in traceback output", result.stdout)
5212+
self.assertNotIn("This should not appear in traceback output", result.stderr)
5213+
finally:
5214+
os.chdir(old_cwd)
51785215

51795216
if __name__ == "__main__":
51805217
unittest.main()

Python/errors.c

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
#include "pycore_traceback.h" // _PyTraceBack_FromFrame()
1414
#include "pycore_unicodeobject.h" // _PyUnicode_Equal()
1515

16+
#include <stdio.h> // fopen, fclose
17+
#include <limits.h> // PATH_MAX
18+
#include <string.h> // strlen
19+
1620
#ifdef MS_WINDOWS
1721
# include <windows.h>
1822
# include <winbase.h>
@@ -1488,25 +1492,27 @@ write_unraisable_exc_file(PyThreadState *tstate, PyObject *exc_type,
14881492
}
14891493
}
14901494

1491-
// Try printing the exception using the stdlib module.
1492-
// If this fails, then we have to use the C implementation.
1493-
PyObject *print_exception_fn = PyImport_ImportModuleAttrString("traceback",
1494-
"_print_exception_bltin");
1495-
if (print_exception_fn != NULL && PyCallable_Check(print_exception_fn)) {
1496-
PyObject *args[2] = {exc_value, file};
1497-
PyObject *result = PyObject_Vectorcall(print_exception_fn, args, 2, NULL);
1498-
int ok = (result != NULL);
1499-
Py_DECREF(print_exception_fn);
1500-
Py_XDECREF(result);
1501-
if (ok) {
1502-
// Nothing else to do
1503-
return 0;
1495+
// Try printing the exception using the stdlib module, but be careful about
1496+
// traceback module shadowing (issue gh-138170). Use safe import check.
1497+
if (_PyTraceback_IsSafeToImport()) {
1498+
PyObject *print_exception_fn = PyImport_ImportModuleAttrString("traceback", "_print_exception_bltin");
1499+
if (print_exception_fn != NULL && PyCallable_Check(print_exception_fn)) {
1500+
PyObject *args[2] = {exc_value, file};
1501+
PyObject *result = PyObject_Vectorcall(print_exception_fn, args, 2, NULL);
1502+
int ok = (result != NULL);
1503+
Py_DECREF(print_exception_fn);
1504+
Py_XDECREF(result);
1505+
if (ok) {
1506+
// Nothing else to do
1507+
return 0;
1508+
}
1509+
}
1510+
else {
1511+
Py_XDECREF(print_exception_fn);
15041512
}
15051513
}
1506-
else {
1507-
Py_XDECREF(print_exception_fn);
1508-
}
1509-
// traceback module failed, fall back to pure C
1514+
1515+
// traceback module failed or unsafe, fall back to pure C
15101516
_PyErr_Clear(tstate);
15111517

15121518
if (exc_tb != NULL && exc_tb != Py_None) {

Python/pythonrun.c

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,20 +1146,21 @@ _PyErr_Display(PyObject *file, PyObject *unused, PyObject *value, PyObject *tb)
11461146
}
11471147
}
11481148

1149-
// Try first with the stdlib traceback module
1150-
PyObject *print_exception_fn = PyImport_ImportModuleAttrString(
1151-
"traceback",
1152-
"_print_exception_bltin");
1153-
if (print_exception_fn == NULL || !PyCallable_Check(print_exception_fn)) {
1154-
goto fallback;
1155-
}
1156-
1157-
PyObject* result = PyObject_CallOneArg(print_exception_fn, value);
1158-
1159-
Py_XDECREF(print_exception_fn);
1160-
if (result) {
1161-
Py_DECREF(result);
1162-
return;
1149+
// Try first with the stdlib traceback module, but be careful about
1150+
// traceback module shadowing (issue gh-138170). Use safe import check.
1151+
if (_PyTraceback_IsSafeToImport()) {
1152+
PyObject *print_exception_fn = PyImport_ImportModuleAttrString("traceback", "_print_exception_bltin");
1153+
if (print_exception_fn != NULL && PyCallable_Check(print_exception_fn)) {
1154+
PyObject* result = PyObject_CallOneArg(print_exception_fn, value);
1155+
Py_DECREF(print_exception_fn);
1156+
if (result) {
1157+
Py_DECREF(result);
1158+
return;
1159+
}
1160+
}
1161+
else {
1162+
Py_XDECREF(print_exception_fn);
1163+
}
11631164
}
11641165
fallback:
11651166
#ifdef Py_DEBUG

Python/traceback.c

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
#include "frameobject.h" // PyFrame_New()
1515

1616
#include "osdefs.h" // SEP
17+
#include <stdio.h> // fopen, fclose
18+
#include <limits.h> // PATH_MAX
19+
#include <string.h> // strlen
1720
#ifdef HAVE_UNISTD_H
1821
# include <unistd.h> // lseek()
1922
#endif
@@ -69,6 +72,37 @@ class traceback "PyTracebackObject *" "&PyTraceback_Type"
6972

7073
#include "clinic/traceback.c.h"
7174

75+
// Helper function to check if it's safe to import traceback module
76+
// Returns 0 if a traceback.py file exists in sys.path[0], 1 if safe
77+
int
78+
_PyTraceback_IsSafeToImport(void)
79+
{
80+
PyObject *sys_path = PySys_GetObject("path");
81+
if (sys_path == NULL || !PyList_Check(sys_path) || PyList_Size(sys_path) == 0) {
82+
return 1; // Default to safe
83+
}
84+
PyObject *first_path = PyList_GetItem(sys_path, 0);
85+
if (first_path == NULL || !PyUnicode_Check(first_path)) {
86+
return 1;
87+
}
88+
const char *path_str = PyUnicode_AsUTF8(first_path);
89+
if (path_str == NULL || strlen(path_str) == 0) {
90+
return 1;
91+
}
92+
// Check if traceback.py exists in the first path directory
93+
char traceback_path[PATH_MAX];
94+
int ret = snprintf(traceback_path, sizeof(traceback_path), "%s/traceback.py", path_str);
95+
if (ret <= 0 || ret >= (int)sizeof(traceback_path)) {
96+
return 1; // Path too long or other error, default to safe
97+
}
98+
FILE *f = fopen(traceback_path, "r");
99+
if (f != NULL) {
100+
fclose(f);
101+
return 0; // Not safe - traceback.py exists
102+
}
103+
return 1; // Safe to import
104+
}
105+
72106
static PyObject *
73107
tb_create_raw(PyTracebackObject *next, PyFrameObject *frame, int lasti,
74108
int lineno)

0 commit comments

Comments
 (0)