1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
//! Panic-hook shim for vCPU worker threads.
//!
//! The crate runs with `panic = "abort"` in release (Cargo.toml), so
//! a panic on any thread tears down the entire VM process without
//! unwinding — Drop impls do not run, and `std::panic::catch_unwind`
//! cannot observe the failure. That leaves a single window between
//! "thread panics" and "libc::abort" during which the user-registered
//! panic hook (`std::panic::set_hook`) runs synchronously on the
//! panicking thread. This module uses that window to flip the
//! per-VM `kill` flag and the per-thread `exited` flag so the
//! watchdog/monitor threads observe a classified shutdown instead of
//! an opaque abort (panic=abort calls `libc::abort`, which raises
//! SIGABRT — not SIGKILL — but the outward signal an observer sees
//! is "process terminated with no cleanup").
//!
//! Primary benefit is *ordering* — the `kill` / `exited` flip
//! happens before `libc::abort`, so any observer that polls those
//! atomics (watchdog, parent join loop) sees a classified shutdown
//! rather than an unexplained abort. User-facing diagnostics
//! (panic message, backtrace) come from the preserved previous-hook
//! chain, not from this module.
//!
//! Scope of work done inside the hook:
//! - Atomic `store(true)` on `kill` and `exited` — non-blocking,
//! allocation-free, and correct under the panicking-thread
//! constraint (any lock acquisition here risks deadlocking against
//! the same thread if it held the lock at the point of panic; any
//! allocation risks triggering a nested panic).
//! - Nothing else. Serial-buffer flush is *not* performed here: the
//! serial state lives behind a `PiMutex` and `PiMutex::lock`'s
//! non-try path would assert-fail if the panic struck mid-`lock`.
//! On a normal exit path the VM cleanup code drains serial; on
//! panic=abort, final serial bytes are intentionally sacrificed for
//! hook correctness.
//!
//! Registration model: `install_once` sets the process-wide hook
//! exactly once (a single test process can spawn multiple VMs
//! sequentially — the hook is installed on the first call and
//! reused). Per-thread context (`VcpuPanicCtx`) is stashed in a
//! `thread_local` from inside the vCPU thread body; the hook reads
//! the thread-local to decide what to signal. Threads that never
//! register leave the thread-local at `None` and the hook falls
//! through to the default.
//!
//! The previous (already-installed) hook is captured at install time
//! and called after our hook runs so standard panic messages /
//! backtraces still reach stderr.
//!
//! Limitation: `std::panic::set_hook` is process-wide. If a future
//! caller of this crate installs their own hook after ours, the
//! previous-hook chain is broken and our signaling is bypassed for
//! any vCPU panic that happens after that. Callers that embed ktstr
//! must install their own hook before spawning vCPU threads, or
//! accept the fall-through.
use RefCell;
use Arc;
use Once;
use ;
use AtomicUsize;
/// Panic-hook callable type. Matches the signature accepted by
/// [`std::panic::set_hook`] and returned by [`std::panic::take_hook`].
type PanicHook = dyn Fn + Send + Sync + 'static;
/// Build the chained panic hook: flip per-thread kill/exited flags (if
/// this thread registered a [`VcpuPanicCtx`]) and then delegate to
/// `prev`. Factored out of [`install_once`] so tests can install a
/// custom `prev` and observe that the chain is not silently dropped.
///
/// # RefCell borrow invariant
///
/// The hook takes a shared `slot.borrow()` on `VCPU_PANIC_CTX`, the
/// per-thread `RefCell<Option<VcpuPanicCtx>>`. That borrow is safe
/// only because the hook is never re-entered while another borrow is
/// active on the same thread's RefCell:
///
/// - `with_vcpu_panic_ctx` scopes its two `borrow_mut()` windows
/// strictly to the set / clear statements and drops them before
/// running `body()`, per that function's documented INVARIANT. Any
/// panic raised inside `body()` therefore finds the RefCell
/// unborrowed, and the hook's `borrow()` cannot conflict.
/// - A panic is delivered to `std`'s hook machinery synchronously on
/// the panicking thread. `std::panic::set_hook` serializes hook
/// registration, and `catch_unwind` / runtime unwinding calls the
/// hook exactly once per `panic!` site before unwinding continues.
/// There is no concurrent second entry into this closure on the
/// same thread to hold a conflicting borrow.
/// - `prev(info)` is the previously-installed process-wide hook
/// captured at `install_once` time. By construction it does not
/// re-enter this module's thread-local (no ktstr code path inside a
/// `prev` hook touches `VCPU_PANIC_CTX`), so the delegation tail
/// cannot recursively panic into our hook.
///
/// If any of those preconditions breaks — a caller holds a `borrow`
/// across a panic site, a runtime gains re-entrant hook dispatch, or
/// a downstream `prev` hook calls back into this module — the
/// `borrow()` here panics, the panic hook double-panics, and under
/// `panic = "abort"` the process aborts without emitting the classified
/// shutdown signal `VcpuPanicCtx` exists to produce. Preserve the
/// invariant.
/// Count of times the `HOOK_ONCE` body executed. The `Once` contract
/// guarantees this reaches 1 and stays there regardless of how many
/// callers invoke [`install_once`], giving tests a stronger assertion
/// than "no panic / no deadlock" for install idempotency.
static INSTALL_COUNT: AtomicUsize = new;
/// Serialize tests that install a custom panic hook via
/// [`install_hook_with_prev_for_test`]. The hook is process-wide, so
/// concurrent manipulation would race. Tests that only rely on the
/// standard `install_once` hook do NOT need this lock — they observe
/// a stable hook via `Once`.
static HOOK_TEST_LOCK: Mutex = new;
thread_local!
/// Flags the panic hook will flip on behalf of a panicking vCPU
/// thread. Clone-cheap — each field is an `Arc<AtomicBool>` shared
/// with the main VM thread and the monitor/watchdog. Fields are
/// `pub(crate)` to match the container's `pub(crate)` visibility;
/// nothing outside the `vmm` module observes this struct directly.
///
/// INVARIANT: Every field's `Drop` must be panic-free. `VcpuPanicCtx`
/// is owned by the `VCPU_PANIC_CTX` thread-local slot and dropped
/// when that slot is cleared — potentially during unwinding of a
/// `body()` panic in `with_vcpu_panic_ctx`. A panicking `Drop` in
/// that window produces a double-panic: the unwind-in-progress plus
/// the Drop panic → `std` aborts the process (even under the
/// test-profile `panic = "unwind"` setting), and the classified
/// shutdown signal this type exists to produce never reaches the
/// watchdog. `Arc<AtomicBool>` satisfies the invariant — its Drop is
/// an atomic decrement + optional `Box` deallocation, neither of
/// which panics. Any new field must uphold the same guarantee.
pub
static HOOK_ONCE: Once = new;
/// Install the vCPU panic hook if it has not already been installed.
/// Idempotent — safe to call from every `spawn_ap_threads`
/// invocation; `Once` gates the actual registration.
///
/// Install convention: callers MUST ensure no other panic hook is
/// installed process-wide AFTER this call. Any later
/// [`std::panic::set_hook`] replaces ours as the active hook; the
/// replacement sees ours as its `prev`, but for vCPU-thread panics our
/// hook is no longer invoked first, so the classified-shutdown
/// signaling documented on [`VcpuPanicCtx`] is bypassed. Embedders
/// with their own panic hook must install it BEFORE `install_once` so
/// our hook sits on top of theirs in the chain.
pub
/// Install a custom `prev` hook wrapped by [`make_hook`] directly,
/// bypassing [`HOOK_ONCE`]. Test-only helper for verifying the
/// prev-hook chain fires on a panic that enters the vCPU hook.
/// Callers must hold [`HOOK_TEST_LOCK`] and restore the previous hook
/// via [`std::panic::set_hook`] before releasing the lock.
/// Register `ctx` for the current thread, run `body`, then clear the
/// registration. Any panic inside `body` is observed by the hook
/// installed by [`install_once`]; regardless of whether `body`
/// returns normally or unwinds, a Drop guard clears the thread-local
/// before this function's stack frame exits so a future reuse of
/// this OS thread (unusual but possible if a runtime recycles
/// threads) does not carry stale context into an unrelated panic.
///
/// INVARIANT: `body()` must not hold a `borrow` or `borrow_mut` on
/// `VCPU_PANIC_CTX` across a potential panic — the hook needs a
/// `borrow()` to read the context, and an outstanding borrow would
/// make that `borrow()` panic (turning the hook into a nested
/// panic, which under panic=abort aborts without signaling). The
/// set- and clear-site `borrow_mut` windows scope strictly to one
/// statement each: the `set` releases before `body()` runs, and the
/// guard's `clear` runs after the hook has already fired and
/// released its shared borrow. The panic window inside `body()`
/// never overlaps a mutable borrow. Callers inside `body` must not
/// re-enter this module's thread local.
///
/// RAII via `CtxGuard`: the previous formulation cleared the slot
/// with an unconditional statement after `body()`, which was skipped
/// when `body()` unwound under the test profile (`panic = "unwind"`).
/// That left stale context in the thread-local for the next reuse
/// of this OS thread; if the runtime recycled the thread onto an
/// unrelated panic, the hook would fire flags that weren't meant for
/// it. Clearing via a Drop guard closes that window — Drop runs on
/// both the normal-return path and the unwinding path.
pub