Skip to content

Inappropriate panic!s in Thread::new on certain unix platforms #160793

Description

@maxdexh

Found after discussing code surrounding #160219 on zulip.

Edit: It is inappropriate for Thread::new to ever panic, because we guarantee that thread::current, which uses it, does not call the global allocator reentrantly. The memory leak on unwind, which this issue was originally about, is not that important.


The implementation of Thread::new manually initializes a struct behind MaybeUninit, which means that if Parker::new_in_place unwinds, the name's memory (essentially Option<CString>), now held by the previously written name field behind MaybeUninit, is leaked.

let inner = unsafe {
let mut arc = Arc::<Inner, _>::new_uninit_in(System);
let ptr = Arc::get_mut_unchecked(&mut arc).as_mut_ptr();
(&raw mut (*ptr).name).write(name);
(&raw mut (*ptr).id).write(id);
Parker::new_in_place(&raw mut (*ptr).parker);
Pin::new_unchecked(arc.assume_init())
};

Note that this is possible, on certain unix platforms:

pub unsafe fn new_in_place(parker: *mut Parker) {
parker.write(Parker {
state: AtomicUsize::new(EMPTY),
lock: Mutex::new(),
cvar: Condvar::new(),
});
Pin::new_unchecked(&mut (*parker).cvar).init();
}

which calls

pub unsafe fn init(self: Pin<&mut Self>) {
use crate::mem::MaybeUninit;
struct AttrGuard<'a>(pub &'a mut MaybeUninit<libc::pthread_condattr_t>);
impl Drop for AttrGuard<'_> {
fn drop(&mut self) {
unsafe {
let result = libc::pthread_condattr_destroy(self.0.as_mut_ptr());
assert_eq!(result, 0);
}
}
}
unsafe {
let mut attr = MaybeUninit::<libc::pthread_condattr_t>::uninit();
let r = libc::pthread_condattr_init(attr.as_mut_ptr());
assert_eq!(r, 0);
let attr = AttrGuard(&mut attr);
let r = libc::pthread_condattr_setclock(attr.0.as_mut_ptr(), Self::CLOCK);
assert_eq!(r, 0);
let r = libc::pthread_cond_init(self.raw(), attr.0.as_ptr());
assert_eq!(r, 0);
}
}

which uses assert_eq to ensure that the initialization of the Condvar succeeds (it can fail, e.g. from resource exhaustion). Note that the method itself expects failures are possible here. It uses a guard struct to ensure that the resources of the condattr are not leaked on unwind.


Furthermore, the implementation of std::thread::current seems to assume that Thread::new never unwinds, because it sets a flag to detect reentrance, which is never reset if an unwind occurs:

fn init_current(current: *mut ()) -> Thread {
if current == NONE {
CURRENT.set(BUSY);
// If the thread ID was initialized already, use it.
let id = id::get_or_init();
let thread = Thread::new(id, None);
// Make sure that `crate::rt::thread_cleanup` will be run, which will
// call `drop_current`.
crate::sys::thread_local::guard::enable();
CURRENT.set(thread.clone().into_raw().cast_mut());
thread
} else if current == BUSY {


The first issue should be fixable by reordering the fields' initialization logic, and the second one by adding a drop guard that unsets the busy state.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-threadArea: `std::thread`C-bugCategory: This is a bug.I-memleakIssue: Runtime memory leak without `mem::forget`.I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessT-libsRelevant to the library team, which will review and decide on the PR/issue.needs-triageThis issue may need triage. Remove it if it has been sufficiently triaged.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions