1
1
#include <Python.h>
2
2
#include <frameobject.h>
3
3
#include <patchlevel.h>
4
+ #include <stdbool.h>
4
5
5
6
#ifdef _WIN32
6
7
#define DD_TRACE_INSTALLED_PREFIX "\\ddtrace\\"
17
18
#define GET_LINENO (frame ) PyFrame_GetLineNumber((PyFrameObject*)frame)
18
19
#define GET_FRAME (tstate ) PyThreadState_GetFrame(tstate)
19
20
#define GET_PREVIOUS (frame ) PyFrame_GetBack(frame)
21
+ #define FRAME_INCREF (frame ) Py_INCREF(frame)
20
22
#define FRAME_DECREF (frame ) Py_DecRef(frame)
21
23
#define FRAME_XDECREF (frame ) Py_XDECREF(frame)
22
24
#define FILENAME_DECREF (filename ) Py_DecRef(filename)
@@ -38,6 +40,7 @@ GET_FILENAME(PyFrameObject* frame)
38
40
#define GET_FRAME (tstate ) tstate->frame
39
41
#define GET_PREVIOUS (frame ) frame->f_back
40
42
#define GET_FILENAME (frame ) frame->f_code->co_filename
43
+ #define FRAME_INCREF (frame )
41
44
#define FRAME_DECREF (frame )
42
45
#define FRAME_XDECREF (frame )
43
46
#define FILENAME_DECREF (filename )
@@ -50,93 +53,188 @@ GET_FILENAME(PyFrameObject* frame)
50
53
#endif
51
54
#endif
52
55
56
+ // Python standard library path
57
+ static char * STDLIB_PATH = NULL ;
58
+ static ssize_t STDLIB_PATH_LEN = 0 ;
59
+
60
+ // Python site-packages path
61
+ static char * PURELIB_PATH = NULL ;
62
+ static ssize_t PURELIB_PATH_LEN = 0 ;
63
+
53
64
/**
54
- * get_file_and_line
55
- *
56
- * Get the filename (path + filename) and line number of the original wrapped
57
- *function to report it.
58
- *
59
- * @return Tuple, string and integer.
60
- **/
61
- static PyObject *
62
- get_file_and_line (PyObject * Py_UNUSED (module ), PyObject * cwd_obj )
65
+ * Checks if the filename is special.
66
+ * For example, a frozen module (`<frozen 'os'>`), a template (`<template>`), etc.
67
+ */
68
+ static inline bool
69
+ _is_special_frame (const char * filename )
63
70
{
64
- PyThreadState * tstate = PyThreadState_Get ();
65
- if (!tstate ) {
66
- goto exit_0 ;
67
- }
71
+ return filename && strncmp (filename , "<" , strlen ("<" )) == 0 ;
72
+ }
68
73
69
- int line ;
70
- PyObject * filename_o = NULL ;
71
- PyObject * result = NULL ;
72
- PyObject * cwd_bytes = NULL ;
73
- char * cwd = NULL ;
74
+ static inline bool
75
+ _is_ddtrace_filename (const char * filename )
76
+ {
77
+ return filename && strstr (filename , DD_TRACE_INSTALLED_PREFIX ) != NULL && strstr (filename , TESTS_PREFIX ) == NULL ;
78
+ }
79
+
80
+ static inline bool
81
+ _is_site_packages_filename (const char * filename )
82
+ {
83
+ const bool res = filename && PURELIB_PATH && strncmp (filename , PURELIB_PATH , PURELIB_PATH_LEN ) == 0 ;
84
+ return res ;
85
+ }
86
+
87
+ static inline bool
88
+ _is_stdlib_filename (const char * filename )
89
+ {
90
+ // site-packages is often a subdirectory of stdlib directory, so stdlib
91
+ // path is defined as prefixed by stdlib and not prefixed by purelib.
92
+ // TODO: As of Python 3.10, we could use sys.stdlib_module_names.
93
+ const bool res = filename && STDLIB_PATH && !_is_site_packages_filename (filename ) &&
94
+ strncmp (filename , STDLIB_PATH , STDLIB_PATH_LEN ) == 0 ;
95
+ return res ;
96
+ }
74
97
75
- if (!PyUnicode_FSConverter (cwd_obj , & cwd_bytes )) {
76
- goto exit_0 ;
98
+ static char *
99
+ get_sysconfig_path (const char * name )
100
+ {
101
+ PyObject * sysconfig_mod = PyImport_ImportModule ("sysconfig" );
102
+ if (!sysconfig_mod ) {
103
+ return NULL ;
77
104
}
78
- cwd = PyBytes_AsString (cwd_bytes );
79
- if (!cwd ) {
80
- goto exit_0 ;
105
+
106
+ PyObject * path = PyObject_CallMethod (sysconfig_mod , "get_path" , "s" , name );
107
+ if (!path ) {
108
+ Py_DECREF (sysconfig_mod );
109
+ return NULL ;
81
110
}
82
111
83
- PyFrameObject * frame = GET_FRAME (tstate );
84
- if (!frame ) {
85
- goto exit_0 ;
112
+ const char * path_str = PyUnicode_AsUTF8 (path );
113
+ char * res = NULL ;
114
+ if (path_str ) {
115
+ res = strdup (path_str );
86
116
}
117
+ Py_DECREF (path );
118
+ Py_DECREF (sysconfig_mod );
119
+ return res ;
120
+ }
87
121
122
+ /**
123
+ * Gets a reference to a PyFrameObject and walks up the stack until a relevant frame is found.
124
+ *
125
+ * Returns a new reference to the PyFrameObject.
126
+ *
127
+ * The caller is not responsible for DECREF'ing the given PyFrameObject, but it is responsible for
128
+ * DECREF'ing the returned PyFrameObject.
129
+ */
130
+ static PyFrameObject *
131
+ _find_relevant_frame (PyFrameObject * frame , bool allow_site_packages )
132
+ {
88
133
while (NULL != frame ) {
89
- filename_o = GET_FILENAME (frame );
134
+ PyObject * filename_o = GET_FILENAME (frame );
90
135
if (!filename_o ) {
91
- goto exit ;
136
+ FRAME_DECREF (frame );
137
+ return NULL ;
92
138
}
93
139
const char * filename = PyUnicode_AsUTF8 (filename_o );
94
- if ((( strstr ( filename , DD_TRACE_INSTALLED_PREFIX ) != NULL && strstr (filename , TESTS_PREFIX ) == NULL ) ) ||
95
- (strstr ( filename , SITE_PACKAGES_PREFIX ) != NULL || strstr (filename , cwd ) == NULL )) {
140
+ if (_is_special_frame ( filename ) || _is_ddtrace_filename (filename ) || _is_stdlib_filename ( filename ) ||
141
+ (! allow_site_packages && _is_site_packages_filename (filename ) )) {
96
142
PyFrameObject * prev_frame = GET_PREVIOUS (frame );
97
143
FRAME_DECREF (frame );
98
144
FILENAME_DECREF (filename_o );
99
145
frame = prev_frame ;
100
146
continue ;
101
147
}
102
- /*
103
- frame->f_lineno will not always return the correct line number
104
- you need to call PyCode_Addr2Line().
105
- */
106
- line = GET_LINENO (frame );
107
- PyObject * line_obj = Py_BuildValue ("i" , line );
108
- if (!line_obj ) {
109
- goto exit ;
110
- }
111
- result = PyTuple_Pack (2 , filename_o , line_obj );
148
+ FILENAME_DECREF (filename_o );
112
149
break ;
113
150
}
114
- if (result == NULL ) {
115
- goto exit_0 ;
151
+ return frame ;
152
+ }
153
+
154
+ static PyObject *
155
+ _get_result_tuple (PyFrameObject * frame )
156
+ {
157
+ PyObject * result = NULL ;
158
+ PyObject * filename_o = NULL ;
159
+ PyObject * line_o = NULL ;
160
+ PyObject * funcname_o = NULL ;
161
+ PyObject * classname_o = NULL ;
162
+
163
+ filename_o = GET_FILENAME (frame );
164
+ if (!filename_o ) {
165
+ goto error ;
116
166
}
117
167
118
- exit :
119
- Py_DecRef (cwd_bytes );
120
- FRAME_XDECREF (frame );
168
+ // frame->f_lineno will not always return the correct line number
169
+ // you need to call PyCode_Addr2Line().
170
+ int line = GET_LINENO (frame );
171
+ line_o = Py_BuildValue ("i" , line );
172
+ if (!line_o ) {
173
+ goto error ;
174
+ }
175
+ result = PyTuple_Pack (2 , filename_o , line_o );
176
+
177
+ error :
121
178
FILENAME_XDECREF (filename_o );
179
+ Py_XDECREF (line_o );
122
180
return result ;
181
+ }
123
182
124
- exit_0 :; // fix: "a label can only be part of a statement and a declaration is not a statement" error
125
- // Return "", -1
126
- PyObject * line_obj = Py_BuildValue ("i" , -1 );
127
- filename_o = PyUnicode_FromString ("" );
128
- result = PyTuple_Pack (2 , filename_o , line_obj );
129
- Py_DecRef (cwd_bytes );
183
+ /**
184
+ * get_file_and_line
185
+ *
186
+ * Get the filename (path + filename) and line number of the original wrapped
187
+ *function to report it.
188
+ *
189
+ * @return Tuple, string and integer.
190
+ **/
191
+ static PyObject *
192
+ get_file_and_line (PyObject * Py_UNUSED (module ), PyObject * Py_UNUSED (args ))
193
+ {
194
+ PyFrameObject * frame = NULL ;
195
+ PyFrameObject * backup_frame = NULL ;
196
+ PyObject * result = NULL ;
197
+ PyThreadState * tstate = PyThreadState_Get ();
198
+ if (!tstate ) {
199
+ goto exit ;
200
+ }
201
+
202
+ frame = GET_FRAME (tstate );
203
+ if (!frame ) {
204
+ goto exit ;
205
+ }
206
+
207
+ // Skip all frames until the first non-ddtrace and non-stdlib frame.
208
+ // Store that frame as backup (if any). If there is no better frame, fallback to this.
209
+ // This happens, for example, when the vulnerability is in a package installed in site-packages.
210
+ frame = _find_relevant_frame (frame , true);
211
+ if (NULL == frame ) {
212
+ goto exit ;
213
+ }
214
+ backup_frame = frame ;
215
+ FRAME_INCREF (backup_frame );
216
+
217
+ // Continue skipping until we find a frame that is both non-ddtrace and non-site-packages.
218
+ frame = _find_relevant_frame (frame , false);
219
+ if (NULL == frame ) {
220
+ frame = backup_frame ;
221
+ backup_frame = NULL ;
222
+ } else {
223
+ FRAME_DECREF (backup_frame );
224
+ }
225
+
226
+ result = _get_result_tuple (frame );
227
+
228
+ exit :
130
229
FRAME_XDECREF (frame );
131
- FILENAME_XDECREF (filename_o );
132
- Py_DecRef (line_obj );
133
230
return result ;
134
231
}
135
232
136
- static PyMethodDef StacktraceMethods [] = {
137
- { "get_info_frame" , (PyCFunction )get_file_and_line , METH_O , "stacktrace functions" },
138
- { NULL , NULL , 0 , NULL }
139
- };
233
+ static PyMethodDef StacktraceMethods [] = { { "get_info_frame" ,
234
+ (PyCFunction )get_file_and_line ,
235
+ METH_NOARGS ,
236
+ "Stacktrace function: returns (filename, line, method, class)" },
237
+ { NULL , NULL , 0 , NULL } };
140
238
141
239
static struct PyModuleDef stacktrace = { PyModuleDef_HEAD_INIT ,
142
240
"ddtrace.appsec._iast._stacktrace" ,
@@ -150,5 +248,13 @@ PyInit__stacktrace(void)
150
248
PyObject * m = PyModule_Create (& stacktrace );
151
249
if (m == NULL )
152
250
return NULL ;
251
+ STDLIB_PATH = get_sysconfig_path ("stdlib" );
252
+ if (STDLIB_PATH ) {
253
+ STDLIB_PATH_LEN = strlen (STDLIB_PATH );
254
+ }
255
+ PURELIB_PATH = get_sysconfig_path ("purelib" );
256
+ if (PURELIB_PATH ) {
257
+ PURELIB_PATH_LEN = strlen (PURELIB_PATH );
258
+ }
153
259
return m ;
154
260
}
0 commit comments