Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4ff3512

Browse files
authored
fix(profiling): use-after-free with Closure trampolines (DataDog#2280)
* fix(profiling): use-after-free with Closure trampolines * Remove comment about the crash being unclear
1 parent 0ff963f commit 4ff3512

2 files changed

Lines changed: 63 additions & 6 deletions

File tree

profiling/src/lib.rs

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,6 +1028,24 @@ extern "C" fn interrupt_function_wrapper(execute_data: *mut zend::zend_execute_d
10281028
}
10291029
}
10301030

1031+
/// Returns true if the func tied to the execute_data is a trampoline.
1032+
/// # Safety
1033+
/// This is only safe to execute _before_ executing the trampoline, because the trampoline may
1034+
/// free the `execute_data.func` _without_ setting it to NULL:
1035+
/// https://heap.space/xref/PHP-8.2/Zend/zend_closures.c?r=af2110e6#60-63
1036+
/// So no code can inspect the func after the call has been made, which is why you would call this function: find out before you
1037+
/// call the function if indeed you need to skip certain code after it has been executed.
1038+
unsafe fn execute_data_func_is_trampoline(execute_data: *const zend::zend_execute_data) -> bool {
1039+
if execute_data.is_null() {
1040+
return false;
1041+
}
1042+
1043+
if (*execute_data).func.is_null() {
1044+
return false;
1045+
}
1046+
return ((*(*execute_data).func).common.fn_flags & zend::ZEND_ACC_CALL_VIA_TRAMPOLINE) != 0;
1047+
}
1048+
10311049
/// Overrides the engine's zend_execute_internal hook in order to process pending VM interrupts
10321050
/// while the internal function is still on top of the call stack. The VM does not process the
10331051
/// interrupt until the call returns so that it could theoretically jump to a different opcode,
@@ -1041,12 +1059,23 @@ extern "C" fn execute_internal(
10411059
execute_data: *mut zend::zend_execute_data,
10421060
return_value: *mut zend::zval,
10431061
) {
1044-
// Safety: PREV_EXECUTE_INTERNAL was written during minit, doesn't change during runtime.
1045-
unsafe {
1046-
let prev_execute_internal = *PREV_EXECUTE_INTERNAL.as_mut_ptr();
1047-
prev_execute_internal(execute_data, return_value);
1048-
}
1049-
interrupt_function(execute_data);
1062+
// SAFETY: called before executing the trampoline.
1063+
let leaf_frame = if unsafe { execute_data_func_is_trampoline(execute_data) } {
1064+
// SAFETY: if is_trampoline is set, then there must be a valid execute_data.
1065+
unsafe { *execute_data }.prev_execute_data
1066+
} else {
1067+
execute_data
1068+
};
1069+
1070+
// SAFETY: PREV_EXECUTE_INTERNAL was written during minit, doesn't change during runtime.
1071+
let prev_execute_internal = unsafe { *PREV_EXECUTE_INTERNAL.as_mut_ptr() };
1072+
1073+
// SAFETY: calling prev_execute without modification will be safe.
1074+
unsafe { prev_execute_internal(execute_data, return_value) };
1075+
1076+
// See safety section of `execute_data_func_is_trampoline` docs for why the leaf frame is used
1077+
// instead of the execute_data ptr.
1078+
interrupt_function(leaf_frame);
10501079
}
10511080

10521081
#[cfg(test)]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
[profiling] use-after-free for inspecting after closure trampoline is called.
3+
--DESCRIPTION--
4+
The code for Closure::__invoke will free the `execute_data->func` before it
5+
returns, but it does not set it to null, except in debug builds:
6+
https://heap.space/xref/PHP-8.2/Zend/zend_closures.c?r=af2110e6#60-63
7+
8+
Our zend_execute_internal hook inspected the func after the call has been made,
9+
potentially triggering the issue. This test will likely only fail under asan.
10+
--SKIPIF--
11+
<?php
12+
if (!extension_loaded('datadog-profiling'))
13+
echo "skip: test requires Datadog Continuous Profiler\n";
14+
if (PHP_VERSION_ID < 80200)
15+
echo "skip: test requires PHP 8.2+\n";
16+
?>
17+
--INI--
18+
datadog.profiling.enabled=1
19+
datadog.profiling.experimental_allocation_enabled=0
20+
--FILE--
21+
<?php
22+
23+
$closure = Closure::fromCallable('sleep');
24+
$closure->__invoke(1);
25+
echo "Done.\n";
26+
?>
27+
--EXPECT--
28+
Done.

0 commit comments

Comments
 (0)