Skip to main content

plat/
plib.rs

1// plib -- plugin libraries and the crash guard: the portable part
2//                                        [src/plat/plib.s; docs/modules/PLUGIN_LOADER.md]
3//
4//   lib_open(path) -> Lib / Err(LIBE_*)    Windows: LoadLibraryExW without DllMain / imports
5//                                          (win_lib.rs); Linux: own ELF ET_DYN loader
6//                                          (linux_elfload.rs)
7//   lib_sym(h, name) -> address | 0        exported symbol (own export / .dynsym lookup)
8//   lib_close(h)                           unload (also when a Lib is dropped)
9//   lib_seg(h, addr) -> (end of the image segment holding addr (0 = outside), LIBA_*)
10//                                          -- check every pointer a plugin hands over (data:
11//                                          LIBA_R, code: LIBA_X) before dereferencing it
12//   guard_init()                           install the OS fault handler once (VEH on
13//                                          Windows, SIGSEGV / SIGBUS / SIGILL / SIGFPE /
14//                                          SIGTRAP on Linux; linux_guard.rs)
15//   guard_call(f, a1, a2, a3, a4, info) -> (rax, rdx)
16//        calls the code at address f with the Win64 ABI (on Linux too: the plugin ABI) as
17//        f(a1, a2, a3, a4); a fault inside it on this thread (also in nested host code)
18//        resumes here with rax = rdx = ERR_FAULT and info (GuardInfo) filled; otherwise
19//        info.kind = 0 and rax / rdx are those of f.  Nested calls are allowed.  ERR_LIMIT
20//        when the thread table is full (GT_MAX threads).
21//   guard_tag() -> u64                     info.tag of this thread's innermost active guard
22//                                          (0 = none)
23//
24// guard_call pushes a guard record (rsp / rbp of its trampoline, xmm6-15, MXCSR, x87
25// control word, tag) and links it as the innermost guard of the calling thread in a table
26// {OS thread id -> record} (thread id = TEB ClientId on Windows, gettid on Linux).  The
27// handlers find the thread's entry by its id, unlink the innermost record (a second fault
28// goes to the enclosing guard), store the fault into it and resume the thread at
29// jfgui_guard_fault_ip with rsp / rbp of the trampoline's body.  That code restores x87,
30// MXCSR, DF and xmm6-15 and returns through the epilogue, which restores the caller's
31// callee-saved registers.  NOT a sandbox: the guard catches faults, nothing else (see the
32// docs: corrupt state stays corrupt, hangs are not detected).
33//
34// Safety contract: guard_call runs untrusted machine code at an arbitrary address; it is a
35// safe function only in the sense the assembly API is: the caller hands it addresses of
36// loaded plugin code (lib_sym) or of its own `extern "win64"` functions.  Pointers a plugin
37// returns are checked with lib_seg before the host reads through them.
38
39use core::sync::atomic::{AtomicU32, AtomicU64, Ordering};
40
41pub const ERR_FAULT: i32 = -64; // the guarded call faulted (plugin disabled)
42
43pub const LIBA_R: u32 = 1; // lib_seg flags
44pub const LIBA_W: u32 = 2;
45pub const LIBA_X: u32 = 4;
46
47pub const LIBE_IO: u32 = 1; // file missing / unreadable / larger than LIB_FILE_MAX
48pub const LIBE_FORMAT: u32 = 2; // not an x86-64 ELF shared object / PE DLL, or damaged
49pub const LIBE_DEPS: u32 = 3; // needs other libraries / undefined symbols
50pub const LIBE_RELOC: u32 = 4; // unsupported relocation
51pub const LIBE_NOMEM: u32 = 5;
52pub const LIBE_INIT: u32 = 6; // TLS, initialisers or an interpreter (not supported)
53pub const LIBE_MAP: u32 = 7; // mmap / mprotect failed
54pub const LIB_FILE_MAX: u64 = 64 << 20;
55pub const LH_SEGMAX: usize = 16;
56
57pub const GK_NONE: u32 = 0;
58pub const GK_ACCESS: u32 = 1; // access violation / SIGSEGV
59pub const GK_ILLEGAL: u32 = 2; // illegal / privileged instruction / SIGILL
60pub const GK_ARITH: u32 = 3; // division by zero, overflow, FP trap / SIGFPE
61pub const GK_BUS: u32 = 4; // misalignment / SIGBUS
62pub const GK_BREAK: u32 = 5; // breakpoint (int3) / SIGTRAP
63pub const GK_STACK: u32 = 6; // stack overflow (Windows)
64pub const GT_MAX: usize = 128;
65
66/// one segment of a loaded image: [start, end), LIBA_*
67#[derive(Clone, Copy, Default, Debug)]
68pub struct LibSeg {
69    pub start: usize,
70    pub end: usize,
71    pub flags: u32,
72}
73
74/// a loaded plugin library (LIBH)
75pub struct Lib {
76    pub(crate) base: usize,
77    pub(crate) span: usize,
78    pub(crate) os: usize, // HMODULE (Windows) / 0
79    pub(crate) nseg: u32,
80    #[allow(dead_code)]
81    pub(crate) nsym: u32, // .dynsym entries (Linux)
82    pub(crate) symtab: usize, // .dynsym (Linux) / export directory RVA (Windows)
83    #[allow(dead_code)]
84    pub(crate) strtab: usize, // .dynstr (Linux)
85    pub(crate) strsz: usize, // its size / the export directory's size (Windows)
86    pub(crate) segs: [LibSeg; LH_SEGMAX],
87}
88
89impl Lib {
90    pub(crate) fn empty() -> Lib {
91        Lib { base: 0, span: 0, os: 0, nseg: 0, nsym: 0, symtab: 0, strtab: 0, strsz: 0, segs: [LibSeg::default(); LH_SEGMAX] }
92    }
93
94    /// the image base
95    pub fn base(&self) -> usize {
96        self.base
97    }
98
99    /// bytes mapped from the base
100    pub fn span(&self) -> usize {
101        self.span
102    }
103
104    /// the segments (Windows: the headers, then every section)
105    pub fn segs(&self) -> &[LibSeg] {
106        &self.segs[..(self.nseg as usize).min(LH_SEGMAX)]
107    }
108}
109
110impl Drop for Lib {
111    fn drop(&mut self) {
112        crate::lib_unload(self);
113    }
114}
115
116/// unload a library (dropping the Lib does the same)  [plat_lib_close]
117pub fn lib_close(h: Lib) {
118    drop(h)
119}
120
121/// the end of the segment of h holding addr (0 = none) and its LIBA_* flags  [plat_lib_seg]
122pub fn lib_seg(h: &Lib, addr: usize) -> (usize, u32) {
123    for s in h.segs() {
124        if addr >= s.start && addr < s.end {
125            return (s.end, s.flags);
126        }
127    }
128    (0, 0)
129}
130
131// ---------------------------------------------------------------- the guard
132
133/// GUARDINFO: what a guarded call hit; tag is an input (guard_tag)
134#[repr(C)]
135#[derive(Clone, Copy, Default, Debug)]
136pub struct GuardInfo {
137    pub kind: u32, // GK_* (0 = no fault)
138    pub code: u32, // NTSTATUS (Windows) / signal number (Linux)
139    pub pc: u64,   // faulting instruction
140    pub addr: u64, // faulting data address (access faults) or 0
141    pub tag: u64,  // in: tag of this guard (guard_tag)
142}
143
144/// the guard record (GR_* of plugin.inc; the trampoline and the handlers use these offsets)
145#[repr(C, align(16))]
146pub(crate) struct GuardRec {
147    pub(crate) prev: u64, // 0: enclosing guard of this thread
148    pub(crate) rsp: u64,  // 8: rsp / rbp of the trampoline's body
149    pub(crate) rbp: u64,  // 16
150    pub(crate) ent: u64,  // 24: thread table entry
151    pub(crate) kind: u32, // 32: GK_* (written by the fault handler)
152    pub(crate) code: u32, // 36
153    pub(crate) pc: u64,   // 40
154    pub(crate) addr: u64, // 48
155    tag: u64,             // 56
156    mxcsr: u32,           // 64
157    fcw: u16,             // 68
158    pad: [u8; 10],
159    xmm: [u8; 160], // 80: xmm6..xmm15
160    rax: u64,       // 240: results of the call
161    rdx: u64,       // 248
162}
163
164const _: () = assert!(core::mem::size_of::<GuardRec>() == 256);
165
166/// one thread: OS thread id, innermost active guard record (0 = none)
167#[repr(C)]
168pub(crate) struct GtEnt {
169    pub(crate) tid: AtomicU64,
170    pub(crate) cur: AtomicU64,
171}
172
173#[allow(clippy::declare_interior_mutable_const)]
174const GT0: GtEnt = GtEnt { tid: AtomicU64::new(0), cur: AtomicU64::new(0) };
175static GT: [GtEnt; GT_MAX] = [GT0; GT_MAX];
176static GT_N: AtomicU32 = AtomicU32::new(0);
177static GUARD_ON: AtomicU32 = AtomicU32::new(0);
178
179// The trampoline (Win64 ABI on both OSes): jfgui_guard_run(rec, f, args) -> 0 returned /
180// 1 faulted.  Frame like the assembly's PROLOG 16: [rsp+0..63] outgoing arguments, [rsp+64]
181// the record pointer (the fault path finds it there: only rsp / rbp are restored).
182core::arch::global_asm!(
183    ".globl jfgui_guard_run",
184    ".globl jfgui_guard_fault_ip",
185    "jfgui_guard_run:",
186    "    push rbp",
187    "    mov rbp, rsp",
188    "    push rbx",
189    "    push rsi",
190    "    push rdi",
191    "    push r12",
192    "    push r13",
193    "    push r14",
194    "    push r15",
195    "    sub rsp, 88",
196    "    mov [rsp + 64], rcx",
197    "    mov rsi, rcx",
198    "    mov r12, rdx",
199    "    mov r13, r8",
200    "    mov [rsi + 8], rsp",
201    "    mov [rsi + 16], rbp",
202    "    mov dword ptr [rsi + 64], 0",
203    "    mov dword ptr [rsi + 68], 0",
204    "    stmxcsr dword ptr [rsi + 64]",
205    "    fnstcw word ptr [rsi + 68]",
206    "    movdqu [rsi + 80], xmm6",
207    "    movdqu [rsi + 96], xmm7",
208    "    movdqu [rsi + 112], xmm8",
209    "    movdqu [rsi + 128], xmm9",
210    "    movdqu [rsi + 144], xmm10",
211    "    movdqu [rsi + 160], xmm11",
212    "    movdqu [rsi + 176], xmm12",
213    "    movdqu [rsi + 192], xmm13",
214    "    movdqu [rsi + 208], xmm14",
215    "    movdqu [rsi + 224], xmm15",
216    "    mov rbx, [rsi + 24]",
217    "    mov [rbx + 8], rsi", // armed: GT_CUR = the record
218    "    mov rcx, [r13]",
219    "    mov rdx, [r13 + 8]",
220    "    mov r8, [r13 + 16]",
221    "    mov r9, [r13 + 24]",
222    "    call r12",
223    // normal return: rsp is intact; rbp / xmm6-15 are restored even if f broke the ABI
224    "    mov rsi, [rsp + 64]",
225    "    mov rbp, [rsi + 16]",
226    "    mov [rsi + 240], rax",
227    "    mov [rsi + 248], rdx",
228    "    mov rbx, [rsi + 24]",
229    "    mov rcx, [rsi]",
230    "    mov [rbx + 8], rcx", // unlinked: GT_CUR = the enclosing record
231    "    call 2f",
232    "    cld",
233    "    xor eax, eax",
234    "    jmp 3f",
235    // the fault handlers resume a faulting guarded thread here (rsp, rbp of the body; the
236    // record is already unlinked)
237    "jfgui_guard_fault_ip:",
238    "    cld",
239    "    fninit",
240    "    mov rsi, [rsp + 64]",
241    "    fldcw word ptr [rsi + 68]",
242    "    ldmxcsr dword ptr [rsi + 64]",
243    "    call 2f",
244    "    mov eax, 1",
245    "3:",
246    "    lea rsp, [rbp - 56]",
247    "    pop r15",
248    "    pop r14",
249    "    pop r13",
250    "    pop r12",
251    "    pop rdi",
252    "    pop rsi",
253    "    pop rbx",
254    "    pop rbp",
255    "    ret",
256    // xmm6..xmm15 from the record at rsi; leaf
257    "2:",
258    "    movdqu xmm6, [rsi + 80]",
259    "    movdqu xmm7, [rsi + 96]",
260    "    movdqu xmm8, [rsi + 112]",
261    "    movdqu xmm9, [rsi + 128]",
262    "    movdqu xmm10, [rsi + 144]",
263    "    movdqu xmm11, [rsi + 160]",
264    "    movdqu xmm12, [rsi + 176]",
265    "    movdqu xmm13, [rsi + 192]",
266    "    movdqu xmm14, [rsi + 208]",
267    "    movdqu xmm15, [rsi + 224]",
268    "    ret",
269);
270
271unsafe extern "win64" {
272    fn jfgui_guard_run(rec: *mut GuardRec, f: usize, args: *const [u64; 4]) -> u64;
273    fn jfgui_guard_fault_ip();
274}
275
276/// where the OS fault handlers resume a faulting guarded thread
277pub(crate) fn guard_fault_ip() -> usize {
278    jfgui_guard_fault_ip as *const () as usize
279}
280
281/// the entry of thread tid or None (for the OS fault handlers: no allocation, no locks)
282/// [guard_lookup]
283pub(crate) fn guard_lookup(tid: u64) -> Option<&'static GtEnt> {
284    let n = (GT_N.load(Ordering::SeqCst) as usize).min(GT_MAX);
285    GT[..n].iter().find(|e| e.tid.load(Ordering::SeqCst) == tid)
286}
287
288/// this thread's entry (claimed on first use) or None when the table is full  [guard_self]
289fn guard_self() -> Option<&'static GtEnt> {
290    let tid = crate::plat_tid();
291    if let Some(e) = guard_lookup(tid) {
292        return Some(e);
293    }
294    let i = GT_N.fetch_add(1, Ordering::SeqCst) as usize;
295    if i >= GT_MAX {
296        GT_N.store(GT_MAX as u32, Ordering::SeqCst);
297        return None;
298    }
299    GT[i].cur.store(0, Ordering::SeqCst);
300    GT[i].tid.store(tid, Ordering::SeqCst);
301    Some(&GT[i])
302}
303
304/// install the fault handler (idempotent, thread safe)  [plat_guard_init]
305pub fn guard_init() -> i32 {
306    if GUARD_ON.compare_exchange(0, 1, Ordering::SeqCst, Ordering::SeqCst).is_ok() {
307        crate::guard_os_install();
308    }
309    0
310}
311
312/// info.tag of the innermost active guard of this thread (0 = none)  [plat_guard_tag]
313pub fn guard_tag() -> u64 {
314    let Some(e) = guard_lookup(crate::plat_tid()) else { return 0 };
315    let r = e.cur.load(Ordering::SeqCst);
316    if r == 0 {
317        return 0;
318    }
319    // SAFETY: an active record lives on this thread's stack (guard_call's frame) until the
320    // guarded call returns, and only this thread reads it here.
321    unsafe { (*(r as *const GuardRec)).tag }
322}
323
324/// f(a1, a2, a3, a4) under the crash guard (see the file header)  [plat_guard_call]
325pub fn guard_call(f: usize, a1: u64, a2: u64, a3: u64, a4: u64, info: Option<&mut GuardInfo>) -> (u64, u64) {
326    if GUARD_ON.load(Ordering::SeqCst) == 0 {
327        guard_init();
328    }
329    let Some(ent) = guard_self() else {
330        let e = crate::ERR_LIMIT as i64 as u64;
331        return (e, e);
332    };
333    if ent.cur.load(Ordering::SeqCst) == 0 {
334        crate::guard_thread(); // outermost guard: per-thread setup
335    }
336    let mut rec = GuardRec {
337        prev: ent.cur.load(Ordering::SeqCst),
338        rsp: 0,
339        rbp: 0,
340        ent: ent as *const GtEnt as u64,
341        kind: 0,
342        code: 0,
343        pc: 0,
344        addr: 0,
345        tag: info.as_ref().map_or(0, |i| i.tag),
346        mxcsr: 0,
347        fcw: 0,
348        pad: [0; 10],
349        xmm: [0; 160],
350        rax: 0,
351        rdx: 0,
352    };
353    let args = [a1, a2, a3, a4];
354    // SAFETY: the trampoline links rec into this thread's entry for the call and unlinks it
355    // on both paths (return: itself; fault: the handler); rec and args outlive the call.
356    // What f does is the caller's contract (see the file header).
357    let faulted = unsafe { jfgui_guard_run(&mut rec, f, &args) };
358    let rec = &rec;
359    if faulted == 0 {
360        if let Some(i) = info {
361            i.kind = 0;
362            i.code = 0;
363            i.pc = 0;
364            i.addr = 0;
365        }
366        return (rec.rax, rec.rdx);
367    }
368    if let Some(i) = info {
369        i.kind = rec.kind;
370        i.code = rec.code;
371        i.pc = rec.pc;
372        i.addr = rec.addr;
373    }
374    let e = ERR_FAULT as i64 as u64;
375    (e, e)
376}
377
378/// (for the OS fault handlers) unlink the innermost record of the faulting thread and store
379/// the fault into it -> (rsp, rbp to resume with), None when the thread has no active guard
380pub(crate) fn guard_hit(tid: u64, kind: u32, code: u32, pc: u64, addr: u64) -> Option<(u64, u64)> {
381    let e = guard_lookup(tid)?;
382    let r = e.cur.load(Ordering::SeqCst);
383    if r == 0 {
384        return None;
385    }
386    let gr = r as *mut GuardRec;
387    // SAFETY: an active record of the faulting thread (its stack, above the fault); the
388    // handler runs on that thread.
389    unsafe {
390        e.cur.store((*gr).prev, Ordering::SeqCst); // unlink: a second fault goes further out
391        (*gr).kind = kind;
392        (*gr).code = code;
393        (*gr).pc = pc;
394        (*gr).addr = addr;
395        Some(((*gr).rsp, (*gr).rbp))
396    }
397}
398
399/// run the Rust closure f under the crash guard -> Ok(result), Err(ERR_FAULT) when it faulted
400/// (info filled), Err(ERR_LIMIT) when the thread table is full (f did not run).  A fault
401/// abandons f's frames without running destructors (what they own leaks): meant for host
402/// code that may touch plugin memory (the loader fuzz runs lib_open / lib_sym under it).
403pub fn guard_run<R, F: FnOnce() -> R>(f: F, info: Option<&mut GuardInfo>) -> Result<R, i32> {
404    struct Ctx<F, R> {
405        f: Option<F>,
406        r: Option<R>,
407    }
408    extern "win64" fn thunk<R, F: FnOnce() -> R>(p: u64) -> u64 {
409        // SAFETY: p is the Ctx of guard_run below, alive for the whole guarded call.
410        let c = unsafe { &mut *(p as *mut Ctx<F, R>) };
411        if let Some(f) = c.f.take() {
412            c.r = Some(f());
413        }
414        0
415    }
416    let mut c = Ctx { f: Some(f), r: None };
417    guard_call(thunk::<R, F> as *const () as usize, &mut c as *mut Ctx<F, R> as u64, 0, 0, 0, info);
418    match c.r.take() {
419        Some(r) => Ok(r),
420        None if c.f.is_some() => Err(crate::ERR_LIMIT), // the table was full: f did not run
421        None => Err(ERR_FAULT),                         // f was taken by the faulted call
422    }
423}
424
425// pn-sys: the host side of the plugin API (PaintCraft's PNAPI) gets addresses from plugin code
426// -- records it passes back (SURFACE, IMGDEC, JOB), strings, buffers on its stack -- that
427// may lie anywhere; these two touch such memory under the crash guard.
428/// copy len bytes at any address under the crash guard -> None when addr is 0, the copy
429/// faulted or the memory for the copy is missing  (pn-sys)
430pub fn guard_read(addr: usize, len: usize) -> Option<alloc::vec::Vec<u8>> {
431    if addr == 0 {
432        return None;
433    }
434    let mut v = alloc::vec::Vec::new();
435    if v.try_reserve_exact(len).is_err() {
436        return None;
437    }
438    let r = guard_run(
439        || {
440            // SAFETY: a fault inside the copy is caught by the guard (v stays empty: its
441            // length is set only after the copy).
442            let s = unsafe { core::slice::from_raw_parts(addr as *const u8, len) };
443            v.extend_from_slice(s);
444        },
445        None,
446    );
447    if r.is_ok() && v.len() == len { Some(v) } else { None }
448}
449
450/// write data at any address under the crash guard -> false when addr is 0 or it faulted
451/// (pn-sys)
452pub fn guard_write(addr: usize, data: &[u8]) -> bool {
453    if addr == 0 {
454        return false;
455    }
456    // SAFETY: a fault inside the copy is caught by the guard; the caller hands over memory
457    // the plugin API documents as writable (a record or buffer the plugin passed).
458    guard_run(|| unsafe { core::ptr::copy_nonoverlapping(data.as_ptr(), addr as *mut u8, data.len()) }, None).is_ok()
459}
460
461/// copy len bytes at addr of the image of h -- only when they lie in one readable segment
462/// (lib_seg), else None (pointers a plugin hands over)
463pub fn lib_read(h: &Lib, addr: usize, len: usize) -> Option<alloc::vec::Vec<u8>> {
464    let (end, fl) = lib_seg(h, addr);
465    if end == 0 || fl & LIBA_R == 0 || len > end - addr {
466        return None;
467    }
468    // SAFETY: [addr, addr + len) lies in a readable segment of the loaded image.
469    Some(unsafe { core::slice::from_raw_parts(addr as *const u8, len) }.to_vec())
470}
471
472// ---------------------------------------------------------------- test aids (plattest)
473// Win64 functions to run under guard_call: 0 sum (rax = a1 + 2 a2 + 3 a3 + 4 a4, rdx = a1 ^
474// a4), 1 write to [a1], 2 ud2, 3 int3, 4 integer division by zero, 5 endless recursion in
475// 4 KB frames (stack overflow), 6 clobber every callee-saved register, xmm6-15, MXCSR (round
476// down), the x87 control word (single precision) and DF, then write to [a1]
477core::arch::global_asm!(
478    ".globl jfgui_gt_code",
479    "jfgui_gt_code:",
480    "0:",
481    "    lea rax, [rcx + rdx*2]",
482    "    lea r10, [r8 + r8*2]",
483    "    add rax, r10",
484    "    lea rax, [rax + r9*4]",
485    "    mov rdx, rcx",
486    "    xor rdx, r9",
487    "    ret",
488    ".p2align 4",
489    "1:",
490    "    mov dword ptr [rcx], 0x0BADF00D",
491    "    ret",
492    ".p2align 4",
493    "2:",
494    "    ud2",
495    "    ret",
496    ".p2align 4",
497    "3:",
498    "    int3",
499    "    ret",
500    ".p2align 4",
501    "4:",
502    "    xor ecx, ecx",
503    "    mov eax, 1",
504    "    xor edx, edx",
505    "    div ecx",
506    "    ret",
507    ".p2align 4",
508    "5:",
509    "    sub rsp, 4096",
510    "    mov [rsp], rax",
511    "    call 5b",
512    "    add rsp, 4096",
513    "    ret",
514    ".p2align 4",
515    "6:",
516    "    mov rax, 0x5A5A5A5A5A5A5A5A",
517    "    mov rbx, rax",
518    "    mov rsi, rax",
519    "    mov rdi, rax",
520    "    mov rbp, rax",
521    "    mov r12, rax",
522    "    mov r13, rax",
523    "    mov r14, rax",
524    "    mov r15, rax",
525    "    movq xmm6, rax",
526    "    movq xmm7, rax",
527    "    movq xmm8, rax",
528    "    movq xmm9, rax",
529    "    movq xmm10, rax",
530    "    movq xmm11, rax",
531    "    movq xmm12, rax",
532    "    movq xmm13, rax",
533    "    movq xmm14, rax",
534    "    movq xmm15, rax",
535    "    push 0x3F80",
536    "    ldmxcsr dword ptr [rsp]",
537    "    mov dword ptr [rsp], 0x7F",
538    "    fldcw word ptr [rsp]",
539    "    add rsp, 8",
540    "    std",
541    "    mov dword ptr [rcx], eax",
542    "    cld",
543    "    ret",
544    ".p2align 4",
545    ".globl jfgui_gt_table",
546    "jfgui_gt_table:",
547    "    lea rax, [rip + 0b]",
548    "    lea rdx, [rip + 1b]",
549    "    lea r8, [rip + 2b]",
550    "    lea r9, [rip + 3b]",
551    "    mov [rcx], rax",
552    "    mov [rcx + 8], rdx",
553    "    mov [rcx + 16], r8",
554    "    mov [rcx + 24], r9",
555    "    lea rax, [rip + 4b]",
556    "    lea rdx, [rip + 5b]",
557    "    lea r8, [rip + 6b]",
558    "    mov [rcx + 32], rax",
559    "    mov [rcx + 40], rdx",
560    "    mov [rcx + 48], r8",
561    "    ret",
562    // -> rax MXCSR, rdx x87 control word | DF << 16
563    ".globl jfgui_gt_state",
564    "jfgui_gt_state:",
565    "    sub rsp, 8",
566    "    stmxcsr dword ptr [rsp]",
567    "    mov eax, [rsp]",
568    "    fnstcw word ptr [rsp + 4]",
569    "    movzx edx, word ptr [rsp + 4]",
570    "    pushfq",
571    "    pop rcx",
572    "    and ecx, 0x400",
573    "    shl ecx, 6",
574    "    or edx, ecx",
575    "    shl rdx, 32",
576    "    or rax, rdx",
577    "    add rsp, 8",
578    "    ret",
579);
580
581unsafe extern "win64" {
582    fn jfgui_gt_table(t: *mut [usize; 7]);
583    fn jfgui_gt_state() -> u64;
584}
585
586/// (test aid) the address of guard test function k (see above), 0 for an unknown k
587pub fn guard_test_code(k: u32) -> usize {
588    let mut t = [0usize; 7];
589    // SAFETY: fills the 7 addresses.
590    unsafe { jfgui_gt_table(&mut t) };
591    t.get(k as usize).copied().unwrap_or(0)
592}
593
594/// (test aid) this thread's MXCSR, x87 control word and direction flag
595pub fn guard_test_state() -> (u32, u32, bool) {
596    // SAFETY: reads the control registers only.
597    let v = unsafe { jfgui_gt_state() };
598    let d = (v >> 32) as u32;
599    (v as u32, d & 0xFFFF, d >> 16 & 1 != 0)
600}