From 05bf2a660b3714cce11585e66277f2c2c515b7da Mon Sep 17 00:00:00 2001 From: Michal 'vorner' Vaner Date: Wed, 9 Dec 2020 10:11:17 +0100 Subject: [PATCH] Add the signal-hook-sys crate For C code that extracts stuff. Because the libc crate is not really very helpful there: * The SI_* constants are missing. * The si_pid/si_uid methods are available only on few selected targets, not everywhere. --- .github/workflows/test.yaml | 9 +- .gitignore | 1 + Cargo.lock | 9 ++ Cargo.toml | 3 + ci-check.sh | 6 +- signal-hook-sys/Cargo.toml | 13 ++ signal-hook-sys/build.rs | 5 + signal-hook-sys/src/extract.c | 51 ++++++++ signal-hook-sys/src/lib.rs | 123 ++++++++++++++++++ .../{exfiltrator.rs => exfiltrator/mod.rs} | 3 + src/iterator/exfiltrator/origin.rs | 123 ++++++++++++++++++ src/iterator/mod.rs | 8 +- 12 files changed, 348 insertions(+), 6 deletions(-) create mode 100644 signal-hook-sys/Cargo.toml create mode 100644 signal-hook-sys/build.rs create mode 100644 signal-hook-sys/src/extract.c create mode 100644 signal-hook-sys/src/lib.rs rename src/iterator/{exfiltrator.rs => exfiltrator/mod.rs} (99%) create mode 100644 src/iterator/exfiltrator/origin.rs diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index cc2743d..1f2ad6a 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -21,7 +21,10 @@ jobs: - stable - beta - nightly + # Introduction of self: Arc<..>, needed for the iterator module - 1.36.0 + # Introduction of non_exhaustive, used at certain exfiltrators + - 1.40.0 runs-on: ${{ matrix.os }} @@ -170,7 +173,7 @@ jobs: uses: Swatinem/rust-cache@v1 - name: Run clippy linter - run: cargo clippy --all --tests -- -D clippy::all -D warnings + run: cargo clippy --all --all-features --tests -- -D clippy::all -D warnings # There's bunch of platforms that have some weird quirks (or we don't know # that they don't). While fully compiling and testing on them is a bit of a @@ -210,7 +213,7 @@ jobs: uses: Swatinem/rust-cache@v1 - name: Run the check - run: cargo check --all --tests --target=${{ matrix.target }} + run: cargo check --all --all-features --tests --target=${{ matrix.target }} # Check some either weirder platforms, but these support only the base crate, # not all the fancy async ones. @@ -242,4 +245,4 @@ jobs: uses: Swatinem/rust-cache@v1 - name: Run the check - run: cargo check --tests --target=${{ matrix.target }} + run: cargo check --tests --all-features --target=${{ matrix.target }} diff --git a/.gitignore b/.gitignore index 72ce2b3..c2a55a1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target **/*.rs.bk tags +.ccls-cache diff --git a/Cargo.lock b/Cargo.lock index 447c22e..1bfd3fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -908,6 +908,7 @@ dependencies = [ "libc", "serial_test", "signal-hook-registry 1.2.2", + "signal-hook-sys", ] [[package]] @@ -950,6 +951,14 @@ dependencies = [ "libc", ] +[[package]] +name = "signal-hook-sys" +version = "0.1.0" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "signal-hook-tokio" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 2d70734..aa5f577 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ maintenance = { status = "actively-developed" } [features] default = ["iterator"] iterator = [] +extended-siginfo = ["iterator", "signal-hook-sys"] [workspace] members = [ @@ -27,11 +28,13 @@ members = [ "signal-hook-tokio", "signal-hook-mio", "signal-hook-async-std", + "signal-hook-sys", ] [dependencies] libc = "~0.2" signal-hook-registry = { version = "~1.2", path = "signal-hook-registry" } +signal-hook-sys = { version = "~0.1", path = "signal-hook-sys", optional = true } [dev-dependencies] serial_test = "~0.5" diff --git a/ci-check.sh b/ci-check.sh index 0bfdf44..2525f93 100755 --- a/ci-check.sh +++ b/ci-check.sh @@ -8,16 +8,18 @@ set -ex rm -f Cargo.lock -cargo build --all --exclude signal-hook-async-std +cargo build --all --exclude signal-hook-async-std --exclude signal-hook-sys if [ "$RUST_VERSION" = 1.36.0 ] ; then exit fi -if [ "$OS" = "windows-latest" ] ; then +if [ "$OS" = "windows-latest" ] || [ "$RUST_VERSION" = 1.40.0 ]; then # The async support crates rely on the iterator module # which isn't available for windows. So exclude them # from the build. + + # Also, some dependencies of the runtimes don't like 1.40. EXCLUDE_FROM_BUILD="--exclude signal-hook-mio --exclude signal-hook-tokio --exclude signal-hook-async-std" else EXCLUDE_FROM_BUILD="" diff --git a/signal-hook-sys/Cargo.toml b/signal-hook-sys/Cargo.toml new file mode 100644 index 0000000..6c88c78 --- /dev/null +++ b/signal-hook-sys/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "signal-hook-sys" +version = "0.1.0" +authors = ["Michal 'vorner' Vaner "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +libc = "~0.2" + +[build-dependencies] +cc = "~1" diff --git a/signal-hook-sys/build.rs b/signal-hook-sys/build.rs new file mode 100644 index 0000000..bb4ce6d --- /dev/null +++ b/signal-hook-sys/build.rs @@ -0,0 +1,5 @@ +use cc::Build; + +fn main() { + Build::new().file("src/extract.c").compile("extract"); +} diff --git a/signal-hook-sys/src/extract.c b/signal-hook-sys/src/extract.c new file mode 100644 index 0000000..8f181d8 --- /dev/null +++ b/signal-hook-sys/src/extract.c @@ -0,0 +1,51 @@ +#include +#include +#include + +struct Const { + int native; + // The signal this applies to, or -1 if it applies to anything. + int signal; + uint8_t translated; +}; + +// Warning: must be in sync with the rust source code +struct Const consts[] = { +#ifdef SI_KERNEL + { SI_KERNEL, -1, 1 }, +#endif + { SI_USER, -1, 2 }, +#ifdef SI_TKILL + { SI_TKILL, -1, 3 }, +#endif + { SI_QUEUE, -1, 4 }, + { SI_MESGQ, -1, 5 }, + { CLD_EXITED, SIGCHLD, 6 }, + { CLD_KILLED, SIGCHLD, 7 }, + { CLD_DUMPED, SIGCHLD, 8 }, + { CLD_TRAPPED, SIGCHLD, 9 }, + { CLD_STOPPED, SIGCHLD, 10 }, + { CLD_CONTINUED, SIGCHLD, 11 }, +}; + +uint8_t sighook_signal_cause(const siginfo_t *info) { + const size_t const_len = sizeof consts / sizeof *consts; + size_t i; + for (i = 0; i < const_len; i ++) { + if ( + consts[i].native == info->si_code && + (consts[i].signal == -1 || consts[i].signal == info->si_signo) + ) { + return consts[i].translated; + } + } + return 0; // The "Unknown" variant +} + +pid_t sighook_signal_pid(const siginfo_t *info) { + return info->si_pid; +} + +uid_t sighook_signal_uid(const siginfo_t *info) { + return info->si_uid; +} diff --git a/signal-hook-sys/src/lib.rs b/signal-hook-sys/src/lib.rs new file mode 100644 index 0000000..ddc5b1c --- /dev/null +++ b/signal-hook-sys/src/lib.rs @@ -0,0 +1,123 @@ +//! Low-level internals of [`signal-hook`](https://docs.rs/signal-hook). +//! +//! This crate contains some internal APIs, split off to a separate crate for technical reasons. Do +//! not use directly. There are no stability guarantees, no documentation and you should use +//! `signal-hook` directly. + +#[doc(hidden)] +pub mod internal { + use libc::{pid_t, siginfo_t, uid_t}; + + // Careful: make sure the signature and the constants match the C source + extern "C" { + fn sighook_signal_cause(info: &siginfo_t) -> Cause; + fn sighook_signal_pid(info: &siginfo_t) -> pid_t; + fn sighook_signal_uid(info: &siginfo_t) -> uid_t; + } + + // Warning: must be in sync with the C code + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + #[non_exhaustive] + #[repr(u8)] + pub enum Cause { + Unknown = 0, + Kernel = 1, + User = 2, + TKill = 3, + Queue = 4, + MesgQ = 5, + Exited = 6, + Killed = 7, + Dumped = 8, + Trapped = 9, + Stopped = 10, + Continued = 11, + } + + impl Cause { + // The MacOs doesn't use the SI_* constants and leaves si_code at 0. But it doesn't use an + // union, it has a good-behaved struct with fields and therefore we *can* read the values, + // even though they'd contain nonsense (zeroes). We wipe that out later. + #[cfg(target_os = "macos")] + fn has_process(self) -> bool { + true + } + + #[cfg(not(target_os = "macos"))] + fn has_process(self) -> bool { + use Cause::*; + match self { + Unknown | Kernel => false, + User | TKill | Queue | MesgQ | Exited | Killed | Dumped | Trapped | Stopped + | Continued => true, + } + } + } + + #[derive(Copy, Clone, Debug, Eq, PartialEq)] + #[non_exhaustive] + pub struct Process { + pub pid: pid_t, + pub uid: uid_t, + } + + impl Process { + /** + * Extract the process information. + * + * # Safety + * + * The `info` must have a `si_code` corresponding to some situation that has the `si_pid` + * and `si_uid` filled in. + */ + unsafe fn extract(info: &siginfo_t) -> Self { + Self { + pid: sighook_signal_pid(info), + uid: sighook_signal_uid(info), + } + } + + pub const fn to_u64(self) -> u64 { + let pid = self.pid as u32; // With overflow for negative ones + let uid = self.uid as u32; + ((pid as u64) << 32) | (uid as u64) + } + + pub const fn from_u64(encoded: u64) -> Self { + let pid = ((encoded >> 32) as u32) as _; + let uid = (encoded as u32) as _; + Self { pid, uid } + } + + pub const EMPTY: Self = Self { pid: -1, uid: 0 }; + + pub const NO_PROCESS: Self = Self { pid: -1, uid: 1 }; + } + + #[derive(Clone, Debug, Eq, PartialEq)] + #[non_exhaustive] + pub struct SigInfo { + pub cause: Cause, + pub process: Option, + } + + impl SigInfo { + // Note: shall be async-signal-safe + pub fn extract(info: &siginfo_t) -> Self { + let cause = unsafe { sighook_signal_cause(info) }; + let process = if cause.has_process() { + let process = unsafe { Process::extract(info) }; + // On macos we don't have the si_code to go by, but we can go by the values being + // empty there. + if cfg!(target_os = "macos") && process.pid == 0 && process.uid == 0 { + None + } else { + Some(process) + } + } else { + None + }; + Self { cause, process } + } + } +} diff --git a/src/iterator/exfiltrator.rs b/src/iterator/exfiltrator/mod.rs similarity index 99% rename from src/iterator/exfiltrator.rs rename to src/iterator/exfiltrator/mod.rs index 6d6923d..7ef4ec3 100644 --- a/src/iterator/exfiltrator.rs +++ b/src/iterator/exfiltrator/mod.rs @@ -11,6 +11,9 @@ //! Currently, the trait is sealed and all methods hidden. This is likely temporary, until some //! experience with them is gained. +#[cfg(feature = "extended-siginfo")] +pub mod origin; + use std::sync::atomic::{AtomicBool, Ordering}; use libc::{c_int, siginfo_t}; diff --git a/src/iterator/exfiltrator/origin.rs b/src/iterator/exfiltrator/origin.rs new file mode 100644 index 0000000..7def7b0 --- /dev/null +++ b/src/iterator/exfiltrator/origin.rs @@ -0,0 +1,123 @@ +//! An exfiltrator providing the process that caused the signal. +//! +//! The [`WithOrigin`] is an [`Exfiltrator`][crate::iterator::exfiltrator::Exfiltrator] that +//! provides the information about sending process in addition to the signal number, through the +//! [`Origin`] type. +//! +//! See the [`WithOrigin`] example. + +use std::sync::atomic::{AtomicU64, Ordering}; + +use libc::{c_int, pid_t, siginfo_t, uid_t}; +use signal_hook_sys::internal::{Process as IProcess, SigInfo}; + +use super::sealed::Exfiltrator; + +/// Information about process, as presented in the signal metadata. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct Process { + /// The process ID. + pub pid: pid_t, + + /// The user owning the process. + pub uid: uid_t, +} + +impl From for Process { + fn from(p: IProcess) -> Self { + Self { + pid: p.pid, + uid: p.uid, + } + } +} + +/// Information about a signal and its origin. +/// +/// This is produced by the [`WithOrigin`] exfiltrator. See the example there. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Origin { + /// The signal that happened. + pub signal: c_int, + + /// Information about the process that caused the signal. + /// + /// Note that not all signals are caused by a specific process or have the information + /// available („fault“ signals like `SIGBUS` don't have, any signal may be sent by the kernel + /// instead of a specific process). + /// + /// This is filled in whenever available. For most signals, this is the process that sent the + /// signal (by `kill` or similar), for `SIGCHLD` it is the child that caused the signal. + pub process: Option, + // TODO: Figure out a better encoding somehow and expose other info, including the Cause +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct OriginStorage(AtomicU64); + +impl Default for OriginStorage { + fn default() -> Self { + Self(AtomicU64::new(IProcess::EMPTY.to_u64())) + } +} + +/// The [`Exfiltrator`][crate::iterator::exfiltrator::Exfiltrator] that produces [`Origin`] of +/// signals. +/// +/// # Examples +/// +/// ```rust +/// # use signal_hook::SIGUSR1; +/// # use signal_hook::iterator::SignalsInfo; +/// # use signal_hook::iterator::exfiltrator::origin::WithOrigin; +/// # +/// # fn main() -> Result<(), std::io::Error> { +/// // Subscribe to SIGUSR1, with information about the process. +/// let signals = SignalsInfo::::new(&[SIGUSR1])?; +/// +/// // Send a signal to ourselves. +/// let my_pid = unsafe { libc::getpid() }; +/// unsafe { libc::kill(my_pid, SIGUSR1) }; +/// +/// // Grab the signal and look into the details. +/// let received = signals.forever().next().unwrap(); +/// +/// assert_eq!(SIGUSR1, received.signal); +/// assert_eq!(my_pid, received.process.unwrap().pid); +/// # Ok(()) } +/// ``` +#[derive(Copy, Clone, Debug, Default)] +pub struct WithOrigin; + +unsafe impl Exfiltrator for WithOrigin { + type Storage = OriginStorage; + type Output = Origin; + fn supports_signal(&self, _: c_int) -> bool { + true + } + + fn store(&self, slot: &OriginStorage, _: c_int, info: &siginfo_t) { + let value = SigInfo::extract(info) + .process + .unwrap_or(IProcess::NO_PROCESS) + .to_u64(); + slot.0.store(value, Ordering::SeqCst); + } + + fn load(&self, slot: &OriginStorage, signal: c_int) -> Option { + let value = slot.0.swap(IProcess::EMPTY.to_u64(), Ordering::SeqCst); + match IProcess::from_u64(value) { + IProcess::EMPTY => None, + IProcess::NO_PROCESS => Some(Origin { + signal, + process: None, + }), + process => Some(Origin { + signal, + process: Some(process.into()), + }), + } + } +} diff --git a/src/iterator/mod.rs b/src/iterator/mod.rs index 7afda7f..3301dfb 100644 --- a/src/iterator/mod.rs +++ b/src/iterator/mod.rs @@ -4,7 +4,7 @@ //! the [`SignalsInfo`] structure which is able to iterate over the //! incoming signals. The structure is parametrized by an //! [`Exfiltrator`][self::exfiltrator::Exfiltrator], which specifies what information is returned -//! for each delivered signal. +//! for each delivered signal. Note that some exfiltrators are behind a feature flag. //! //! The [`Signals`] is a type alias for the common case when it is enough to get the signal number. //! @@ -89,6 +89,12 @@ use self::exfiltrator::{Exfiltrator, SignalOnly}; /// The controller handle can be shared between as many threads as you like using its /// [`clone`][Handle::clone] method. /// +/// # Exfiltrators +/// +/// The [`SignalOnly]` provides only the signal number. There are further exfiltrators available in +/// the [`exfiltrator`] module. Note that some of them are behind feature flags that need to be +/// enabled. +/// /// # Examples /// /// ```rust