nros/executor_handle.rs
1//! Issue 1535 — what a `void* executor` handed across the language seam IS.
2//!
3//! Two different objects travel as `void*` and are both called "the executor
4//! handle":
5//!
6//! * a Rust entry's `RuntimeCtx::executor_handle()` — a bare
7//! `*mut Executor<'static>`;
8//! * a C/C++ entry's `rclcpp::global_handle()` (and every tier's `executor`) —
9//! an nros-cpp `CppContext`, which carries the executor at a NON-zero offset,
10//! behind the [`CPP_CONTEXT_TAG`](crate::executor_handle::CPP_CONTEXT_TAG) word at offset 0.
11//!
12//! nros-cpp's own entry points tell them apart by that tag (issue 0436). The
13//! seams in THIS crate that take a handle — the cross-language component
14//! install `__nros_component_<pkg>_install` and the contract-monitor install —
15//! used to cast whatever arrived straight to `*mut Executor<'static>`, so a
16//! C/C++ entry installing a Rust node handed the node a pointer that was off by
17//! the tag. Whether that crashed depended on where rustc happened to put the
18//! fields of a `repr(Rust)` `CppContext`: the mixed Zephyr image SEGVed on its
19//! first publish while every layout that put the executor first ran by luck.
20//!
21//! One answer now, two halves: nros-cpp pins the tag at offset 0 (`repr(C)`)
22//! and exports `nros_cpp_executor_inner`, which the generated C++ entry calls
23//! before it reaches a Rust install; and `executor_from_handle` here REFUSES a
24//! handle that is still tagged, so a caller that forgets is a clean error
25//! rather than a corrupted executor.
26
27// The classification is crate-internal and its one caller needs `rmw-cffi`;
28// it stays un-gated so the `env,std` unit lane, which links no RMW, can test it.
29#![cfg_attr(not(feature = "rmw-cffi"), allow(dead_code))]
30
31use core::ffi::c_void;
32
33/// The word nros-cpp stamps at offset 0 of a live `CppContext` (issue 0436).
34///
35/// Defined HERE, in the lower crate, because both sides read it: nros-cpp
36/// stamps and checks it, and `executor_from_handle` refuses a handle that
37/// carries it. One constant, so the two cannot drift. Arbitrary, but not a
38/// plausible pointer or small integer.
39pub const CPP_CONTEXT_TAG: u64 = 0x6E52_4F53_4350_5001; // "nROSCP\x50\x01"
40
41/// Whether `handle` points at an nros-cpp executor CONTEXT rather than at a bare
42/// `Executor` — i.e. whether its first word is [`CPP_CONTEXT_TAG`].
43///
44/// # Safety
45/// `handle` must be non-null and point to at least 8 readable bytes. Both
46/// handle kinds satisfy that: a `CppContext` starts with the tag, and an
47/// `Executor` is far larger than one word.
48pub(crate) unsafe fn is_cpp_context(handle: *const c_void) -> bool {
49 // Unaligned: an `Executor` on a 32-bit target need not be 8-aligned.
50 unsafe { core::ptr::read_unaligned(handle as *const u64) == CPP_CONTEXT_TAG }
51}
52
53/// Why a handle cannot be used as an `Executor`.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub(crate) enum HandleRefusal {
56 /// The handle was null.
57 Null,
58 /// The handle is an nros-cpp `CppContext`, not an `Executor`. The C/C++
59 /// caller must pass `nros_cpp_executor_inner(handle)` instead.
60 CppContext,
61}
62
63/// Issue 1535 — classify a `void*` handed to a seam that wants a bare
64/// `*mut Executor<'static>`: `Ok(())` when it may be cast, otherwise the
65/// reason it may not.
66///
67/// The ONE check every such seam makes before the cast (see
68/// [`executor_from_handle`]). Split out so the decision is testable without an
69/// executor, which needs an RMW backend linked.
70///
71/// # Safety
72/// `handle` must be null or point to at least 8 readable bytes.
73pub(crate) unsafe fn check_executor_handle(handle: *const c_void) -> Result<(), HandleRefusal> {
74 if handle.is_null() {
75 return Err(HandleRefusal::Null);
76 }
77 if unsafe { is_cpp_context(handle) } {
78 return Err(HandleRefusal::CppContext);
79 }
80 Ok(())
81}
82
83/// Issue 1535 — THE conversion from a `void*` executor handle to the
84/// `Executor` behind it, for every seam in this crate that takes one.
85///
86/// Refuses (and logs, naming `seam`) a null handle or an nros-cpp
87/// `CppContext`; the latter is a caller bug that used to corrupt the executor
88/// silently.
89///
90/// # Safety
91/// `handle` must be null, an nros-cpp executor context, or a live
92/// `*mut Executor<'static>` that nothing else references for the returned
93/// borrow's lifetime.
94#[cfg(feature = "rmw-cffi")]
95pub(crate) unsafe fn executor_from_handle<'a>(
96 handle: *mut c_void,
97 seam: &'static str,
98) -> Result<&'a mut nros_node::Executor<'static>, HandleRefusal> {
99 match unsafe { check_executor_handle(handle) } {
100 Ok(()) => Ok(unsafe { &mut *(handle as *mut nros_node::Executor<'static>) }),
101 Err(refusal) => {
102 if refusal == HandleRefusal::CppContext {
103 nros_log::log_error!(
104 nros_log::get_logger("nros.executor_handle"),
105 "{}: handed an nros-cpp executor CONTEXT where an Executor was expected; a \
106 C/C++ caller must pass nros_cpp_executor_inner(handle) (issue 1535)",
107 seam
108 );
109 }
110 Err(refusal)
111 }
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn a_cpp_context_handle_is_refused() {
121 // What a live `CppContext` looks like to this crate: the tag at offset
122 // 0, then the executor. Before issue 1535 the install seam cast this
123 // straight to `*mut Executor` and wrote through the tag.
124 let ctx: [u64; 4] = [CPP_CONTEXT_TAG, 0, 0, 0];
125 assert_eq!(
126 unsafe { check_executor_handle(ctx.as_ptr().cast()) },
127 Err(HandleRefusal::CppContext)
128 );
129 }
130
131 #[test]
132 fn the_executor_inside_a_cpp_context_is_accepted() {
133 // `nros_cpp_executor_inner` hands over the word AFTER the tag.
134 let ctx: [u64; 4] = [CPP_CONTEXT_TAG, 0x1234, 0, 0];
135 assert_eq!(
136 unsafe { check_executor_handle(ctx[1..].as_ptr().cast()) },
137 Ok(())
138 );
139 }
140
141 #[test]
142 fn a_null_handle_is_refused() {
143 assert_eq!(
144 unsafe { check_executor_handle(core::ptr::null()) },
145 Err(HandleRefusal::Null)
146 );
147 }
148}