Skip to content

Commit 3e98860

Browse files
committed
Auto merge of #75811 - ecstatic-morse:better-dlerror, r=nagisa
Refactor dynamic library error checking on *nix The old code was checking `dlerror` more often than necessary, since (unlike `dlsym`) checking the return value of [`dlopen`](https://www.man7.org/linux/man-pages/man3/dlopen.3.html) is enough to indicate whether an error occurred. In the first commit, I've refactored the code to minimize the number of system calls needed. It should be strictly better than the old version. The second commit is an optional addendum which fixes the issue observed on illumos in #74469, a PR I reviewed that was ultimately closed due to inactivity. I'm not sure how hard we try to work around platform-specific bugs like this, and I believe that, due to the way that `dlerror` is specified in the POSIX standard, libc implementations that want to run on conforming systems cannot call `dlsym` in multi-threaded programs.
2 parents bf43421 + aae6c0f commit 3e98860

File tree

2 files changed

+74
-34
lines changed

2 files changed

+74
-34
lines changed

src/librustc_metadata/dynamic_lib.rs

+73-34
Original file line numberDiff line numberDiff line change
@@ -51,51 +51,90 @@ mod tests;
5151

5252
#[cfg(unix)]
5353
mod dl {
54-
use std::ffi::{CStr, CString, OsStr};
54+
use std::ffi::{CString, OsStr};
5555
use std::os::unix::prelude::*;
56-
use std::ptr;
57-
use std::str;
5856

59-
pub(super) fn open(filename: &OsStr) -> Result<*mut u8, String> {
60-
check_for_errors_in(|| unsafe {
61-
let s = CString::new(filename.as_bytes()).unwrap();
62-
libc::dlopen(s.as_ptr(), libc::RTLD_LAZY) as *mut u8
63-
})
64-
}
57+
// As of the 2017 revision of the POSIX standard (IEEE 1003.1-2017), it is
58+
// implementation-defined whether `dlerror` is thread-safe (in which case it returns the most
59+
// recent error in the calling thread) or not thread-safe (in which case it returns the most
60+
// recent error in *any* thread).
61+
//
62+
// There's no easy way to tell what strategy is used by a given POSIX implementation, so we
63+
// lock around all calls that can modify `dlerror` in this module lest we accidentally read an
64+
// error from a different thread. This is bulletproof when we are the *only* code using the
65+
// dynamic library APIs at a given point in time. However, it's still possible for us to race
66+
// with other code (see #74469) on platforms where `dlerror` is not thread-safe.
67+
mod error {
68+
use std::ffi::CStr;
69+
use std::lazy::SyncLazy;
70+
use std::sync::{Mutex, MutexGuard};
71+
72+
pub fn lock() -> MutexGuard<'static, Guard> {
73+
static LOCK: SyncLazy<Mutex<Guard>> = SyncLazy::new(|| Mutex::new(Guard { _priv: () }));
74+
LOCK.lock().unwrap()
75+
}
6576

66-
fn check_for_errors_in<T, F>(f: F) -> Result<T, String>
67-
where
68-
F: FnOnce() -> T,
69-
{
70-
use std::sync::{Mutex, Once};
71-
static INIT: Once = Once::new();
72-
static mut LOCK: *mut Mutex<()> = ptr::null_mut();
73-
unsafe {
74-
INIT.call_once(|| {
75-
LOCK = Box::into_raw(Box::new(Mutex::new(())));
76-
});
77-
// dlerror isn't thread safe, so we need to lock around this entire
78-
// sequence
79-
let _guard = (*LOCK).lock();
80-
let _old_error = libc::dlerror();
81-
82-
let result = f();
83-
84-
let last_error = libc::dlerror() as *const _;
85-
if ptr::null() == last_error {
86-
Ok(result)
87-
} else {
88-
let s = CStr::from_ptr(last_error).to_bytes();
89-
Err(str::from_utf8(s).unwrap().to_owned())
77+
pub struct Guard {
78+
_priv: (),
79+
}
80+
81+
impl Guard {
82+
pub fn get(&mut self) -> Result<(), String> {
83+
let msg = unsafe { libc::dlerror() };
84+
if msg.is_null() {
85+
Ok(())
86+
} else {
87+
let msg = unsafe { CStr::from_ptr(msg as *const _) };
88+
Err(msg.to_string_lossy().into_owned())
89+
}
90+
}
91+
92+
pub fn clear(&mut self) {
93+
let _ = unsafe { libc::dlerror() };
9094
}
9195
}
9296
}
9397

98+
pub(super) fn open(filename: &OsStr) -> Result<*mut u8, String> {
99+
let s = CString::new(filename.as_bytes()).unwrap();
100+
101+
let mut dlerror = error::lock();
102+
let ret = unsafe { libc::dlopen(s.as_ptr(), libc::RTLD_LAZY | libc::RTLD_LOCAL) };
103+
104+
if !ret.is_null() {
105+
return Ok(ret.cast());
106+
}
107+
108+
// A NULL return from `dlopen` indicates that an error has definitely occurred, so if
109+
// nothing is in `dlerror`, we are racing with another thread that has stolen our error
110+
// message. See the explanation on the `dl::error` module for more information.
111+
dlerror.get().and_then(|()| Err("Unknown error".to_string()))
112+
}
113+
94114
pub(super) unsafe fn symbol(
95115
handle: *mut u8,
96116
symbol: *const libc::c_char,
97117
) -> Result<*mut u8, String> {
98-
check_for_errors_in(|| libc::dlsym(handle as *mut libc::c_void, symbol) as *mut u8)
118+
let mut dlerror = error::lock();
119+
120+
// Unlike `dlopen`, it's possible for `dlsym` to return NULL without overwriting `dlerror`.
121+
// Because of this, we clear `dlerror` before calling `dlsym` to avoid picking up a stale
122+
// error message by accident.
123+
dlerror.clear();
124+
125+
let ret = libc::dlsym(handle as *mut libc::c_void, symbol);
126+
127+
if !ret.is_null() {
128+
return Ok(ret.cast());
129+
}
130+
131+
// If `dlsym` returns NULL but there is nothing in `dlerror` it means one of two things:
132+
// - We tried to load a symbol mapped to address 0. This is not technically an error but is
133+
// unlikely to occur in practice and equally unlikely to be handled correctly by calling
134+
// code. Therefore we treat it as an error anyway.
135+
// - An error has occurred, but we are racing with another thread that has stolen our error
136+
// message. See the explanation on the `dl::error` module for more information.
137+
dlerror.get().and_then(|()| Err("Tried to load symbol mapped to address 0".to_string()))
99138
}
100139

101140
pub(super) unsafe fn close(handle: *mut u8) {

src/librustc_metadata/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#![feature(drain_filter)]
66
#![feature(in_band_lifetimes)]
77
#![feature(nll)]
8+
#![feature(once_cell)]
89
#![feature(or_patterns)]
910
#![feature(proc_macro_internals)]
1011
#![feature(min_specialization)]

0 commit comments

Comments
 (0)