Skip to content

thread_local macro stability precludes safe async signal handling #30003

Open
@mahkoh

Description

@mahkoh

The thread_local! macro accepts arbitrary (non-Sync) objects to be put into thread local storage. It is not hard to construct a case where this causes signal handlers to observe inconsistent state:

extern {
    fn signal(num: i32, handler: extern fn(i32)) -> extern fn(i32);
}

use std::cell::{RefCell};

/// First and second value always the same.
thread_local!(static X: RefCell<(usize,usize)> = RefCell::new((0,0)));

extern fn handler(_: i32) {
    X.with(|x| {
        let x = x.borrow();
        println!("{:?}", *x);
    });
}

fn main() {
    unsafe { signal(2, handler); }
    X.with(|x| {
        let mut x = x.borrow_mut();
        x.0 += 1;
        // raise(2)
        x.1 += 1;
    });
}

RefCell is not signal safe. A mutable borrow will not mark the RefCell as being borrowed in this case. This can be simulated as follows:

  • Compile with -O -C lto
  • In gdb, step to the instruction that stores the second value.
  • signal 2

Expected result: panic/abort/segfault or similar. Actual result: (1, 0) is printed.

Fixing RefCell by adding a memory barrier does not fix the problem since there might be many other non-Sync types that were not written with signal handling in mind and that use unsafe constructs. For correctness, TLS would have to be restricted to types that are async safe via a new marker trait. With such a trait, signal handling would be safe by default in all rust code and all signals handlers could call arbitrary rust functions (as long as said functions don't call non-rust code which might not be async safe.)


This concerns me because adding a signal handler is a safe operation in lrs and all #[thread_local] objects that require mutation are wrapped in a single threaded mutex implementation with interior mutability. And if it is decided that async signal handling is never safe in rust, then #[thread_local] might be stabilized and might also start to accept arbitrary objects which would practically force me to create a full compiler fork for the sake of safety. The current implementation in lrs is already unsafe because the single threaded mutex implementation has to be marked Sync to be placed in a #[thread_local]. For correctness, there would have to be the above mentioned marker trait that restricts what can be put in a #[thread_local].

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-bugCategory: This is a bug.I-needs-decisionIssue: In need of a decision.P-lowLow priorityT-langRelevant to the language team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions