Skip to content

Commit 8977c0c

Browse files
authored
Merge pull request #1071 from vcfxb/fix-tracing-ub
(Breaking Changes) Re-implement tracing wrapper in a safer way, replacing the use of `&str` with `&[u8]`.
2 parents 3b8dc04 + 7be7525 commit 8977c0c

File tree

1 file changed

+74
-18
lines changed

1 file changed

+74
-18
lines changed

src/tracing.rs

+74-18
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
use std::sync::atomic::{AtomicUsize, Ordering};
1+
use std::{
2+
ffi::CStr,
3+
sync::atomic::{AtomicPtr, Ordering},
4+
};
25

3-
use libc::c_char;
6+
use libc::{c_char, c_int};
47

5-
use crate::{panic, raw, util::Binding};
8+
use crate::{panic, raw, util::Binding, Error};
69

710
/// Available tracing levels. When tracing is set to a particular level,
811
/// callers will be provided tracing at the given level and all lower levels.
@@ -57,29 +60,82 @@ impl Binding for TraceLevel {
5760
}
5861
}
5962

60-
//TODO: pass raw &[u8] and leave conversion to consumer (breaking API)
6163
/// Callback type used to pass tracing events to the subscriber.
6264
/// see `trace_set` to register a subscriber.
63-
pub type TracingCb = fn(TraceLevel, &str);
65+
pub type TracingCb = fn(TraceLevel, &[u8]);
6466

65-
static CALLBACK: AtomicUsize = AtomicUsize::new(0);
67+
/// Use an atomic pointer to store the global tracing subscriber function.
68+
static CALLBACK: AtomicPtr<()> = AtomicPtr::new(std::ptr::null_mut());
6669

67-
///
68-
pub fn trace_set(level: TraceLevel, cb: TracingCb) -> bool {
69-
CALLBACK.store(cb as usize, Ordering::SeqCst);
70+
/// Set the global subscriber called when libgit2 produces a tracing message.
71+
pub fn trace_set(level: TraceLevel, cb: TracingCb) -> Result<(), Error> {
72+
// Store the callback in the global atomic.
73+
CALLBACK.store(cb as *mut (), Ordering::SeqCst);
7074

71-
unsafe {
72-
raw::git_trace_set(level.raw(), Some(tracing_cb_c));
73-
}
75+
// git_trace_set returns 0 if there was no error.
76+
let return_code: c_int = unsafe { raw::git_trace_set(level.raw(), Some(tracing_cb_c)) };
7477

75-
return true;
78+
if return_code != 0 {
79+
// Unwrap here is fine since `Error::last_error` always returns `Some`.
80+
Err(Error::last_error(return_code).unwrap())
81+
} else {
82+
Ok(())
83+
}
7684
}
7785

86+
/// The tracing callback we pass to libgit2 (C ABI compatible).
7887
extern "C" fn tracing_cb_c(level: raw::git_trace_level_t, msg: *const c_char) {
79-
let cb = CALLBACK.load(Ordering::SeqCst);
80-
panic::wrap(|| unsafe {
81-
let cb: TracingCb = std::mem::transmute(cb);
82-
let msg = std::ffi::CStr::from_ptr(msg).to_string_lossy();
83-
cb(Binding::from_raw(level), msg.as_ref());
88+
// Load the callback function pointer from the global atomic.
89+
let cb: *mut () = CALLBACK.load(Ordering::SeqCst);
90+
91+
// Transmute the callback pointer into the function pointer we know it to be.
92+
//
93+
// SAFETY: We only ever set the callback pointer with something cast from a TracingCb
94+
// so transmuting back to a TracingCb is safe. This is notably not an integer-to-pointer
95+
// transmute as described in the mem::transmute documentation and is in-line with the
96+
// example in that documentation for casing between *const () to fn pointers.
97+
let cb: TracingCb = unsafe { std::mem::transmute(cb) };
98+
99+
// If libgit2 passes us a message that is null, drop it and do not pass it to the callback.
100+
// This is to avoid ever exposing rust code to a null ref, which would be Undefined Behavior.
101+
if msg.is_null() {
102+
return;
103+
}
104+
105+
// Convert the message from a *const c_char to a &[u8] and pass it to the callback.
106+
//
107+
// SAFETY: We've just checked that the pointer is not null. The other safety requirements are left to
108+
// libgit2 to enforce -- namely that it gives us a valid, nul-terminated, C string, that that string exists
109+
// entirely in one allocation, that the string will not be mutated once passed to us, and that the nul-terminator is
110+
// within isize::MAX bytes from the given pointers data address.
111+
let msg: &CStr = unsafe { CStr::from_ptr(msg) };
112+
113+
// Convert from a CStr to &[u8] to pass to the rust code callback.
114+
let msg: &[u8] = CStr::to_bytes(msg);
115+
116+
// Do the remaining part of this function in a panic wrapper, to catch any panics it produces.
117+
panic::wrap(|| {
118+
// Convert the raw trace level into a type we can pass to the rust callback fn.
119+
//
120+
// SAFETY: Currently the implementation of this function (above) may panic, but is only marked as unsafe to match
121+
// the trait definition, thus we can consider this call safe.
122+
let level: TraceLevel = unsafe { Binding::from_raw(level) };
123+
124+
// Call the user-supplied callback (which may panic).
125+
(cb)(level, msg);
84126
});
85127
}
128+
129+
#[cfg(test)]
130+
mod tests {
131+
use super::TraceLevel;
132+
133+
// Test that using the above function to set a tracing callback doesn't panic.
134+
#[test]
135+
fn smoke() {
136+
super::trace_set(TraceLevel::Trace, |level, msg| {
137+
dbg!(level, msg);
138+
})
139+
.expect("libgit2 can set global trace callback");
140+
}
141+
}

0 commit comments

Comments
 (0)