Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tracing function runtime patch #1060

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8d706a5
Add runtime patch and restore calls on trace transition.
Pavel-Durov Mar 27, 2024
e1fd774
Add docstring in patch module
Pavel-Durov Mar 27, 2024
ee56310
Add message to panic on empty trace in swt.
Pavel-Durov Mar 27, 2024
461448f
Add patch calls to side-traces.
Pavel-Durov Mar 27, 2024
e237278
Add panic if runtime patch is called on non-x86 platform.
Pavel-Durov Mar 27, 2024
0699dbf
Add test to swt patch.
Pavel-Durov Mar 27, 2024
0db3e4f
Add restore assertion to patch test.
Pavel-Durov Mar 27, 2024
6994b59
Add assert to function restore.
Pavel-Durov Mar 27, 2024
0e88a29
Add x86 compile-time guard
Pavel-Durov Mar 29, 2024
86e404b
Add compile-time guard.
Pavel-Durov Mar 29, 2024
3725c71
Remove PROT_EXEC on mprotect.
Pavel-Durov Mar 29, 2024
799644e
Revert "Remove PROT_EXEC on mprotect."
Pavel-Durov Mar 29, 2024
d572cab
Add documentation.
Pavel-Durov Mar 29, 2024
0bd8f36
Add module docstring.
Pavel-Durov Mar 29, 2024
08029de
Merge branch 'master' into tracing-function-runtime-patch
Pavel-Durov Mar 29, 2024
b2aee3d
Encapsulate x86 instructions.
Pavel-Durov Mar 29, 2024
f1794a2
Merge branch 'master' into tracing-function-runtime-patch
Pavel-Durov Apr 7, 2024
c46bb5f
Merge branch 'master' into tracing-function-runtime-patch
Pavel-Durov Apr 9, 2024
40c0696
Remove PROT_EXEC from mprotect.
Pavel-Durov Apr 9, 2024
4401153
Format.
Pavel-Durov Apr 9, 2024
5ead893
Remove not needed cfgs.
Pavel-Durov Apr 11, 2024
b6fcbd4
Add x86_64 guard.
Pavel-Durov Apr 11, 2024
b334bfa
Merge branch 'master' into tracing-function-runtime-patch
Pavel-Durov Apr 11, 2024
6f4bdc3
Set mprotect as read+write on patch.
Pavel-Durov Apr 13, 2024
8e00241
Use Layout::from_size_align for aligned page calculation.
Pavel-Durov Apr 14, 2024
ada68bb
Change match to unwrap().
Pavel-Durov Apr 18, 2024
3e7d023
Recover and panic on mprotect.
Pavel-Durov Apr 18, 2024
18b7fee
Remove +size from start_offset calculation.
Pavel-Durov Apr 18, 2024
e1be48c
Add comments.
Pavel-Durov Apr 18, 2024
0428dc1
Use unwrap over expect.
Pavel-Durov Apr 20, 2024
6be7bef
Early return if mprotect failes to set memmory page as writable.
Pavel-Durov Apr 20, 2024
6f0abb3
Add panic on mprotect failure.
Pavel-Durov Apr 23, 2024
94597ef
Merge branch 'master' into tracing-function-runtime-patch
Pavel-Durov May 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions ykrt/src/mt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ use std::{
thread,
};

#[cfg(tracer_swt)]
#[cfg(target_arch = "x86_64")]
use crate::trace::swt::patch::{patch_trace_function, restore_trace_function};

use parking_lot::{Condvar, Mutex, MutexGuard};
#[cfg(not(all(feature = "yk_testing", not(test))))]
use parking_lot_core::SpinWait;
Expand Down Expand Up @@ -255,6 +259,10 @@ impl MT {
}
TransitionControlPoint::StartTracing(hl) => {
log_jit_state("start-tracing");
#[cfg(tracer_swt)]
unsafe {
restore_trace_function();
}
let tracer = {
let lk = self.tracer.lock();
Arc::clone(&*lk)
Expand All @@ -271,6 +279,11 @@ impl MT {
}
}
TransitionControlPoint::StopTracing => {
#[cfg(tracer_swt)]
unsafe {
patch_trace_function();
}

// Assuming no bugs elsewhere, the `unwrap`s cannot fail, because `StartTracing`
// will have put a `Some` in the `Rc`.
let (hl, thread_tracer, promotions) =
Expand Down Expand Up @@ -316,6 +329,10 @@ impl MT {
Ok(utrace) => {
self.stats.timing_state(TimingState::None);
log_jit_state("stop-side-tracing");
#[cfg(tracer_swt)]
unsafe {
patch_trace_function();
}
self.queue_compile_job(
(utrace, promotions.into_boxed_slice()),
hl,
Expand Down Expand Up @@ -533,6 +550,10 @@ impl MT {
TransitionGuardFailure::NoAction => todo!(),
TransitionGuardFailure::StartSideTracing(hl) => {
log_jit_state("start-side-tracing");
#[cfg(tracer_swt)]
unsafe {
restore_trace_function();
}
let tracer = {
let lk = self.tracer.lock();
Arc::clone(&*lk)
Expand Down
5 changes: 4 additions & 1 deletion ykrt/src/trace/swt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ use std::{
sync::{Arc, LazyLock},
};

#[cfg(target_arch = "x86_64")]
pub(crate) mod patch;
ltratt marked this conversation as resolved.
Show resolved Hide resolved

#[derive(Debug, Eq, PartialEq, Clone)]
struct TracingBBlock {
function_index: usize,
Expand Down Expand Up @@ -105,7 +108,7 @@ impl TraceRecorder for SWTTraceRecorder {
Err(TraceRecorderError::TraceTooLong)
} else if bbs.is_empty() {
// FIXME: who should handle an empty trace?
panic!();
panic!("swt encountered an empty trace!");
} else {
Ok(Box::new(SWTraceIterator::new(bbs)))
}
Expand Down
142 changes: 142 additions & 0 deletions ykrt/src/trace/swt/patch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//! This module provides functionality for dynamically patching and
//! restoring `yk_trace_basicblock` function at runtime. It includes
//! mechanisms to save the original instructions of a target function,
//! patch the target function with new instructions, and restore the
//! original instructions.
//!
//! This is particularly useful in scenarios such as software tracing,
//! where the tracing function may be dynamically modified to have
//! single return instruction and later restored to their original state
//! for performance gain.
//!
//! The module relies on low-level operations like memory protection
//! changes to manipulate function code safely. It is designed to be
//! used with `tracer_swt` configurations, enabling conditional
//! compilation for tracing functionalities.
//!
//! # Warning
//!
//! This module performs low-level memory operations and modifies the
//! execution flow of functions at runtime. Improper use can lead to
//! undefined behaviour, memory corruption, or crashes.

use crate::trace::swt::yk_trace_basicblock;
use libc::{mprotect, size_t, sysconf, PROT_EXEC, PROT_READ, PROT_WRITE, _SC_PAGESIZE};
use std::alloc::Layout;
use std::{ffi::c_void, sync::Once};

// This is used to ensure that the original instructions are only saved once.
static ORIGINAL_INSTRUCTIONS_INIT: Once = Once::new();

// Original instructions of the function that is patched with `PATCH_X86_INSTRUCTIONS`.
static mut ORIGINAL_INSTRUCTIONS: [u8; 1] = [0; 1];

// 0xC3 is a `ret` instruction on x86_64.
static mut PATCH_X86_INSTRUCTIONS: [u8; 1] = [0xC3];

/// This function is used to save the original instructions of a function to .
///
/// # Arguments
///
/// * `function_ptr` - A usize representing the memory address of the function.
/// * `instructions` - A mutable pointer to a u8 where the original instructions will be saved.
/// * `num_of_instructions` - A usize indicating the number of instructions to save.
///
unsafe fn save_original_instructions(
function_ptr: usize,
instructions: *mut u8,
num_of_instructions: usize,
) {
let func_ptr: *const () = function_ptr as *const ();
std::ptr::copy_nonoverlapping(func_ptr as *const u8, instructions, num_of_instructions);
}

/// This function is used to patch a function instructions at runtime.
///
/// # Arguments
///
/// * `function_ptr` - A usize representing the memory address of the function to be patched.
/// * `code` - A constant pointer to a u8 vector where the new instructions are located.
/// * `size` - A size_t indicating the number of bytes to copy from `code`.
///
unsafe fn patch_function(function_ptr: usize, code: *const u8, size: size_t) {
let page_size = sysconf(_SC_PAGESIZE) as usize;

let page_address = (function_ptr & !(page_size - 1)) as *mut c_void;
let start_offset = function_ptr - (page_address as usize) + size;

// This unwrap should be safe since we are using a page that is
// based on function_ptr with a known location.
let layout = Layout::from_size_align(start_offset, page_size)
.expect("Failed to create layout for function memory page");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just use unwrap as all these expects really bloat the code, especially as they shouldn't ever trigger. [As this suggests we almost never use expect: it does have a place, but it's really rare for us.]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it.
updated 👉 0428dc1

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get rid of the other expects introduced in this PR too please?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any other expects only a single unwrap.
Am I missing it somehow?


// Set function memory page as writable.
// Ignoring mprotect call failure.
mprotect(page_address, layout.size(), PROT_READ | PROT_WRITE);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to ignore the return code: we just want to abort changing the machine code. So we need something like if mprotect(...) { return ... } and then recover from there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to check: is just return enough for the system to keep running? If so, let's add a comment to that effect.

Alternatively, if we think "if mprotect has failed, we've got something wrong", then we might want to panic if mprotect fails.

Copy link
Contributor Author

@Pavel-Durov Pavel-Durov Apr 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's ok to return if mrotect as it should be transactional and not leave the memory in corrupted state on error. Unless we consider the "runtime patching" as a must-have for SWT, then we should definitely panic.

I tested it with different invalid memory addresses that resulted in mrotect returning a non-0 result and the test suite passed.
We still risk changing memory that will not necessarily cause mrotect to return with an error but will cause some other part of the process to have invalid memory.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think this runtime patching is an integral part of SWT.
Added panic 👉 6f0abb3

// Copy the new code over
std::ptr::copy_nonoverlapping(code, function_ptr as *mut u8, size);
// Set function memory page as readable
if mprotect(page_address, layout.size(), PROT_READ | PROT_EXEC) != 0 {
panic!("Failed to change memory protection back to executable");
}
}

/// This function is used to patch the `yk_trace_basicblock`
/// function with a single `ret` (0xC3) instruction.
pub(crate) unsafe fn patch_trace_function() {
ORIGINAL_INSTRUCTIONS_INIT.call_once(|| {
save_original_instructions(
yk_trace_basicblock as usize,
ORIGINAL_INSTRUCTIONS.as_mut_ptr(),
1,
);
});
#[cfg(target_arch = "x86_64")]
patch_function(
yk_trace_basicblock as usize,
PATCH_X86_INSTRUCTIONS.as_ptr(),
1,
);
}

/// This function is used to restore the original behavior of a
/// previously patched `yk_trace_basicblock` function.
pub(crate) unsafe fn restore_trace_function() {
ORIGINAL_INSTRUCTIONS_INIT.call_once(|| {
save_original_instructions(
yk_trace_basicblock as usize,
ORIGINAL_INSTRUCTIONS.as_mut_ptr(),
1,
);
});
patch_function(
yk_trace_basicblock as usize,
ORIGINAL_INSTRUCTIONS.as_ptr(),
1,
);
}

#[cfg(test)]
mod patch_tests {
use super::*;

fn test_function() -> i32 {
return 42;
}

#[test]
fn test_runtime_patch() {
unsafe {
assert_eq!(test_function(), 42);
save_original_instructions(
test_function as usize,
ORIGINAL_INSTRUCTIONS.as_mut_ptr(),
1,
);
patch_function(test_function as usize, PATCH_X86_INSTRUCTIONS.as_ptr(), 1);
assert_eq!(test_function(), 0);
patch_function(test_function as usize, ORIGINAL_INSTRUCTIONS.as_ptr(), 1);
assert_eq!(test_function(), 42);
}
}
}