Passed
Pull Request — master (#7)
by Shinji
12:52
created

ExecutorGlobalsReader::readOpline()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 10
nc 2
nop 2
dl 0
loc 19
rs 9.9332
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file is part of the sj-i/php-profiler package.
5
 *
6
 * (c) sji <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace PhpProfiler\Lib\PhpProcessReader\PhpMemoryReader;
15
16
use FFI\CData;
17
use PhpProfiler\Lib\PhpInternals\ZendTypeReader;
18
use PhpProfiler\Lib\Process\MemoryReader\MemoryReaderInterface;
19
use PhpProfiler\Lib\Process\MemoryReader\MemoryReaderException;
20
21
/**
22
 * Class ExecutorGlobalsReader
23
 * @package PhpProfiler\ProcessReader\PhpStateReader
24
 */
25
final class ExecutorGlobalsReader
26
{
27
    private MemoryReaderInterface $memory_reader;
28
    private ZendTypeReader $zend_type_reader;
29
30
    /**
31
     * ExecutorGlobalsReader constructor.
32
     * @param MemoryReaderInterface $memory_reader
33
     * @param ZendTypeReader $zend_type_reader
34
     */
35
    public function __construct(MemoryReaderInterface $memory_reader, ZendTypeReader $zend_type_reader)
36
    {
37
        $this->memory_reader = $memory_reader;
38
        $this->zend_type_reader = $zend_type_reader;
39
    }
40
41
42
    /**
43
     * @param int $pid
44
     * @param int $executor_globals_address
45
     * @return string
46
     * @throws MemoryReaderException
47
     */
48
    public function readCurrentFunctionName(int $pid, int $executor_globals_address): string
49
    {
50
        /** @var \FFI\PhpInternals\zend_executor_globals $eg */
51
        $eg = $this->readExecutorGlobals($pid, $executor_globals_address);
52
53
        /** @var \FFI\PhpInternals\zend_execute_data $current_execute_data */
54
        $current_execute_data = $this->readCurrentExecuteData($pid, $eg);
55
56
        /** @var \FFI\PhpInternals\zend_function $current_function */
57
        $current_function = $this->readCurrentFunction($pid, $current_execute_data);
58
59
        return $this->readFunctionName($pid, $current_function);
60
    }
61
62
    /**
63
     * @param int $pid
64
     * @param int $executor_globals_address
65
     * @return CData
66
     * @throws MemoryReaderException
67
     */
68
    public function readExecutorGlobals(int $pid, int $executor_globals_address): CData
69
    {
70
        $eg_raw = $this->memory_reader->read(
71
            $pid,
72
            $executor_globals_address,
73
            $this->zend_type_reader->sizeOf('zend_executor_globals')
74
        );
75
        return $this->zend_type_reader->readAs('zend_executor_globals', $eg_raw);
76
    }
77
78
    /**
79
     * @param int $pid
80
     * @param CData $eg
81
     * @return CData
82
     * @throws MemoryReaderException
83
     */
84
    public function readCurrentExecuteData(int $pid, CData $eg): CData
85
    {
86
        /**
87
         * @var \FFI\CPointer $current_execute_data_addr
88
         * @var \FFI\PhpInternals\zend_executor_globals $eg
89
         */
90
        $current_execute_data_addr = \FFI::cast('long', $eg->current_execute_data);
91
        $current_execute_data_raw = $this->memory_reader->read(
92
            $pid,
93
            $current_execute_data_addr->cdata,
94
            $this->zend_type_reader->sizeOf('zend_execute_data')
95
        );
96
        return clone $this->zend_type_reader->readAs('zend_execute_data', $current_execute_data_raw);
97
    }
98
99
    /**
100
     * @param int $pid
101
     * @param CData $current_execute_data
102
     * @return CData
103
     * @throws MemoryReaderException
104
     */
105
    public function readCurrentFunction(int $pid, CData $current_execute_data): CData
106
    {
107
        /**
108
         * @var \FFI\CPointer $func_pointer
109
         * @var \FFI\PhpInternals\zend_execute_data $current_execute_data
110
         */
111
        $func_pointer = \FFI::cast('long', $current_execute_data->func);
112
        $current_function_raw = $this->memory_reader->read(
113
            $pid,
114
            $func_pointer->cdata,
115
            $this->zend_type_reader->sizeOf('zend_function')
116
        );
117
        return clone $this->zend_type_reader->readAs('zend_function', $current_function_raw);
118
    }
119
120
    /**
121
     * @param int $pid
122
     * @param CData $current_function
123
     * @return string
124
     * @throws MemoryReaderException
125
     */
126
    public function readFunctionName(int $pid, CData $current_function): string
127
    {
128
        $function_name = '<main>';
129
        $class_name = '';
130
        /**
131
         * @psalm-var \FFI\PhpInternals\zend_function $current_function
132
         * @psalm-var \FFI\CPointer $current_function_name_pointer
133
         */
134
        $current_function_name_pointer = \FFI::cast('long', $current_function->common->function_name);
135
        if ($current_function_name_pointer->cdata !== 0) {
136
            /** @var \FFI\CPointer $current_function_name_pointer */
137
            $current_function_name_zstring = $this->memory_reader->read(
138
                $pid,
139
                $current_function_name_pointer->cdata,
140
                $this->zend_type_reader->sizeOf('zend_string') + 256
141
            );
142
            /** @var \FFI\PhpInternals\zend_string $string */
143
            $string = $this->zend_type_reader->readAs('zend_string', $current_function_name_zstring);
144
            $function_name = \FFI::string($string->val);
0 ignored issues
show
Bug introduced by
The call to FFI::string() has too few arguments starting with size. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

144
            /** @scrutinizer ignore-call */ 
145
            $function_name = \FFI::string($string->val);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
145
            /** @var \FFI\CPointer $current_function_scope_pointer */
146
            $current_function_scope_pointer = \FFI::cast('long', $current_function->common->scope);
147
            if ($current_function_scope_pointer->cdata !== 0) {
148
                $current_function_class_entry = $this->memory_reader->read(
149
                    $pid,
150
                    $current_function_scope_pointer->cdata,
151
                    $this->zend_type_reader->sizeOf('zend_class_entry')
152
                );
153
                /** @var \FFI\PhpInternals\zend_class_entry $class_entry */
154
                $class_entry = $this->zend_type_reader->readAs('zend_class_entry', $current_function_class_entry);
155
                $current_class_name_pointer = \FFI::cast('long', $class_entry->name);
156
                /** @var \FFI\CPointer $current_class_name_pointer */
157
                $current_class_name_zstring = $this->memory_reader->read(
158
                    $pid,
159
                    $current_class_name_pointer->cdata,
160
                    $this->zend_type_reader->sizeOf('zend_string') + 256
161
                );
162
                /** @var \FFI\PhpInternals\zend_string $class_name_string */
163
                $class_name_string = $this->zend_type_reader->readAs('zend_string', $current_class_name_zstring);
164
                $class_name = \FFI::string($class_name_string->val) . '::';
165
            }
166
        }
167
168
        return $class_name . $function_name;
169
    }
170
171
    /**
172
     * @param int $pid
173
     * @param CData $current_function
174
     * @return string
175
     * @throws MemoryReaderException
176
     */
177
    public function readFunctionFile(int $pid, CData $current_function): string
178
    {
179
        /** @psalm-var \FFI\PhpInternals\zend_function $current_function */
180
        $filename = '<internal>';
181
        if ($current_function->type === 2) {
182
            $filename_pointer = \FFI::cast('long', $current_function->op_array->filename);
183
            /** @var \FFI\CPointer $filename_pointer */
184
            $filename_zstring_raw = $this->memory_reader->read(
185
                $pid,
186
                $filename_pointer->cdata,
187
                $this->zend_type_reader->sizeOf('zend_string') + 256
188
            );
189
            /** @var \FFI\PhpInternals\zend_string $filename_zstring */
190
            $filename_zstring = $this->zend_type_reader->readAs('zend_string', $filename_zstring_raw);
191
            $filename = \FFI::string($filename_zstring->val);
0 ignored issues
show
Bug introduced by
The call to FFI::string() has too few arguments starting with size. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

191
            /** @scrutinizer ignore-call */ 
192
            $filename = \FFI::string($filename_zstring->val);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
192
        }
193
        return $filename;
194
    }
195
196
    /**
197
     * @param int $pid
198
     * @param int $executor_globals_address
199
     * @param int $depth
200
     * @return string[]
201
     * @throws MemoryReaderException
202
     */
203
    public function readCallTrace(int $pid, int $executor_globals_address, int $depth): array
204
    {
205
        /** @var \FFI\PhpInternals\zend_executor_globals $eg */
206
        $eg = $this->readExecutorGlobals($pid, $executor_globals_address);
207
208
        /** @var \FFI\PhpInternals\zend_execute_data $current_execute_data */
209
        $current_execute_data = $this->readCurrentExecuteData($pid, $eg);
210
211
        $stack = [];
212
        $stack[] = $current_execute_data;
213
        for ($i = 0; $i < $depth; $i++) {
214
            $current_execute_data = $this->readPreviousExecuteData($pid, $current_execute_data);
215
            if (is_null($current_execute_data)) {
216
                break;
217
            }
218
            $stack[] = $current_execute_data;
219
        }
220
221
        $result = [];
222
        foreach ($stack as $current_execute_data) {
223
            /** @var \FFI\PhpInternals\zend_function $current_function */
224
            $current_function = $this->readCurrentFunction($pid, $current_execute_data);
225
            $lineno = -1;
226
            $file = $this->readFunctionFile($pid, $current_function);
227
            if ($file !== '<internal>') {
228
                $lineno = $this->readOpline($pid, $current_execute_data);
229
            }
230
            $result[] = $this->readFunctionName($pid, $current_function) . " {$file}({$lineno})";
231
        }
232
233
        return $result;
234
    }
235
236
    /**
237
     * @param int $pid
238
     * @param CData $execute_data
239
     * @return CData
240
     * @throws MemoryReaderException
241
     */
242
    public function readPreviousExecuteData(int $pid, CData $execute_data): ?CData
243
    {
244
        /**
245
         * @var \FFI\CPointer $previous_execute_data_addr
246
         * @var \FFI\PhpInternals\zend_execute_data $execute_data
247
         */
248
        $previous_execute_data_addr = \FFI::cast('long', $execute_data->prev_execute_data);
249
        if ($previous_execute_data_addr->cdata == 0) {
250
            return null;
251
        }
252
        $previous_execute_data_raw = $this->memory_reader->read(
253
            $pid,
254
            $previous_execute_data_addr->cdata,
255
            $this->zend_type_reader->sizeOf('zend_execute_data')
256
        );
257
        return clone $this->zend_type_reader->readAs('zend_execute_data', $previous_execute_data_raw);
258
    }
259
260
    /**
261
     * @param int $pid
262
     * @param CData $current_execute_data
263
     * @return int
264
     * @throws MemoryReaderException
265
     */
266
    public function readOpline(int $pid, CData $current_execute_data): int
267
    {
268
        /**
269
         * @psalm-var \FFI\CPointer $opline_addr
270
         * @psalm-var \FFI\PhpInternals\zend_execute_data $current_execute_data
271
         */
272
        $opline_addr = \FFI::cast('long', $current_execute_data->opline);
273
        if ($opline_addr->cdata == 0) {
274
            return -1;
275
        }
276
        $opline_raw = $this->memory_reader->read(
277
            $pid,
278
            $opline_addr->cdata + 24,
279
            4
280
        );
281
        return $opline_raw[0]
282
            + ($opline_raw[1] << 9)
283
            + ($opline_raw[2] << 16)
284
            + ($opline_raw[3] << 24);
285
    }
286
}
287