11/*
2- * Copyright 2025 Datadog, Inc
2+ * Copyright 2025, 2026 Datadog, Inc
33 *
44 * Licensed under the Apache License, Version 2.0 (the "License");
55 * you may not use this file except in compliance with the License.
1616
1717
1818#include " safeAccess.h"
19+ #include < cstdio>
20+ #include < cstdlib>
1921#include < signal.h>
2022#include < ucontext.h>
2123
2224extern " C" int safefetch32_cont (int * adr, int errValue);
2325extern " C" int64_t safefetch64_cont (int64_t * adr, int64_t errValue);
2426
27+ // safecopy_impl copies `len` bytes from `src` to `dst` one byte at a time and
28+ // returns 1 on success. If any load from `src` faults, the signal handler
29+ // redirects the pc anywhere in [safecopy_impl, safecopy_cont) to safecopy_cont,
30+ // which returns 0. Both are leaf routines with no stack frame, so the redirect
31+ // keeps the return address intact.
32+ extern " C" int safecopy_impl (void * dst, const void * src, size_t len);
33+ extern " C" int safecopy_cont (void * dst, const void * src, size_t len);
34+
35+ #ifdef DEBUG
36+ // handle_safefetch protects safeCopy by treating any fault whose pc lands in
37+ // [safecopy_impl, safecopy_cont) as a recoverable read fault. That range is
38+ // only valid if the assembler/linker keeps safecopy_cont strictly above
39+ // safecopy_impl (they are emitted adjacently in one asm block). A future
40+ // toolchain change could in principle reorder them, collapsing the range to
41+ // empty and silently disabling fault protection. Assert the invariant once at
42+ // load time so such a regression aborts loudly at startup instead of turning
43+ // safeCopy into an unprotected crash. Debug builds only: this is a
44+ // toolchain-regression tripwire, not a runtime safety check.
45+ __attribute__ ((constructor))
46+ static void verify_safecopy_range() {
47+ if ((uintptr_t )safecopy_cont <= (uintptr_t )safecopy_impl) {
48+ fprintf (stderr,
49+ " FATAL: safecopy_cont (%p) is not above safecopy_impl (%p); "
50+ " safeCopy fault protection would be lost\n " ,
51+ (void *)safecopy_cont, (void *)safecopy_impl);
52+ abort ();
53+ }
54+ }
55+ #endif // DEBUG
56+
2557#ifdef __APPLE__
2658 #if defined(__x86_64__)
2759 #define current_pc context_rip
@@ -67,6 +99,25 @@ extern "C" int64_t safefetch64_cont(int64_t* adr, int64_t errValue);
6799 _safefetch64_cont:
68100 movq %rsi, %rax
69101 ret
102+ .globl _safecopy_impl
103+ .private_extern _safecopy_impl
104+ _safecopy_impl:
105+ xorl %ecx, %ecx
106+ Lsafecopy_loop:
107+ cmpq %rdx, %rcx
108+ jae Lsafecopy_done
109+ movzbl (%rsi,%rcx,1), %eax
110+ movb %al, (%rdi,%rcx,1)
111+ incq %rcx
112+ jmp Lsafecopy_loop
113+ Lsafecopy_done:
114+ movl $1, %eax
115+ ret
116+ .globl _safecopy_cont
117+ .private_extern _safecopy_cont
118+ _safecopy_cont:
119+ xorl %eax, %eax
120+ ret
70121 )" );
71122 #else
72123 asm (R"(
@@ -95,6 +146,27 @@ extern "C" int64_t safefetch64_cont(int64_t* adr, int64_t errValue);
95146 safefetch64_cont:
96147 movq %rsi, %rax
97148 ret
149+ .globl safecopy_impl
150+ .hidden safecopy_impl
151+ .type safecopy_impl, %function
152+ safecopy_impl:
153+ xorl %ecx, %ecx
154+ .Lsafecopy_loop:
155+ cmpq %rdx, %rcx
156+ jae .Lsafecopy_done
157+ movzbl (%rsi,%rcx,1), %eax
158+ movb %al, (%rdi,%rcx,1)
159+ incq %rcx
160+ jmp .Lsafecopy_loop
161+ .Lsafecopy_done:
162+ movl $1, %eax
163+ ret
164+ .globl safecopy_cont
165+ .hidden safecopy_cont
166+ .type safecopy_cont, %function
167+ safecopy_cont:
168+ xorl %eax, %eax
169+ ret
98170 )" );
99171 #endif // __APPLE__
100172#elif defined(__aarch64__)
@@ -120,6 +192,25 @@ extern "C" int64_t safefetch64_cont(int64_t* adr, int64_t errValue);
120192 _safefetch64_cont:
121193 mov x0, x1
122194 ret
195+ .globl _safecopy_impl
196+ .private_extern _safecopy_impl
197+ _safecopy_impl:
198+ mov x3, xzr
199+ Lsafecopy_loop:
200+ cmp x3, x2
201+ b.hs Lsafecopy_done
202+ ldrb w4, [x1, x3]
203+ strb w4, [x0, x3]
204+ add x3, x3, #1
205+ b Lsafecopy_loop
206+ Lsafecopy_done:
207+ mov w0, #1
208+ ret
209+ .globl _safecopy_cont
210+ .private_extern _safecopy_cont
211+ _safecopy_cont:
212+ mov w0, #0
213+ ret
123214 )" );
124215 #else
125216 asm (R"(
@@ -148,61 +239,39 @@ extern "C" int64_t safefetch64_cont(int64_t* adr, int64_t errValue);
148239 safefetch64_cont:
149240 mov x0, x1
150241 ret
242+ .globl safecopy_impl
243+ .hidden safecopy_impl
244+ .type safecopy_impl, %function
245+ safecopy_impl:
246+ mov x3, xzr
247+ .Lsafecopy_loop:
248+ cmp x3, x2
249+ b.hs .Lsafecopy_done
250+ ldrb w4, [x1, x3]
251+ strb w4, [x0, x3]
252+ add x3, x3, #1
253+ b .Lsafecopy_loop
254+ .Lsafecopy_done:
255+ mov w0, #1
256+ ret
257+ .globl safecopy_cont
258+ .hidden safecopy_cont
259+ .type safecopy_cont, %function
260+ safecopy_cont:
261+ mov w0, #0
262+ ret
151263 )" );
152264 #endif
153265#endif
154266
155267bool SafeAccess::safeCopy (void * dst, const void * src, size_t len) {
156- // Two-sentinel pattern (same as isReadable): a real-data word may equal
157- // one sentinel by chance, but not both — if both fetches return their
158- // sentinel, the access truly faulted.
159- //
160- // All safefetch32 loads issued here use 4-byte-aligned addresses. Pages
161- // are 4 KiB (or 16 KiB on Apple Silicon), both divisible by 4, so an
162- // aligned 4-byte load never spans a page boundary. The only fault
163- // possible is when the aligned address itself lies in an unmapped page;
164- // we never spuriously fault on an over-read past `src + len`.
165- static const int32_t SENT_A = (int32_t )0x55AA55AA ;
166- static const int32_t SENT_B = (int32_t )0xAA55AA55 ;
167- uint8_t * d = (uint8_t *)dst;
168- const uint8_t * s = (const uint8_t *)src;
169- size_t i = 0 ;
170-
171- // Front fixup: if `src` is not 4-byte aligned, fetch at the previous
172- // aligned address (1..3 bytes before src). That address lies in the
173- // same 4-byte word as src — and since pages are 4-byte aligned, in
174- // the same page as src. The leading k bytes of the fetched word lie
175- // before the caller's range and are discarded via the +k offset; they
176- // never reach `dst`.
177- size_t k = (uintptr_t )s & 3u ;
178- if (k != 0 && i < len) {
179- int32_t * aligned = (int32_t *)(s - k);
180- int32_t v1 = safefetch32_impl (aligned, SENT_A );
181- int32_t v2 = safefetch32_impl (aligned, SENT_B );
182- if (v1 == SENT_A && v2 == SENT_B ) {
183- return false ;
184- }
185- size_t take = (4 - k < len) ? (4 - k) : len;
186- memcpy (d, ((const uint8_t *)&v1) + k, take);
187- i = take;
188- }
189-
190- // Middle + tail: (s + i) is now 4-byte aligned. The final iteration may
191- // load up to 3 over-read bytes past `src + len`, but those bytes sit in
192- // the same 4-byte-aligned word and therefore the same page as the bytes
193- // we actually wanted — never a fault from the over-read alone.
194- while (i < len) {
195- int32_t * aligned = (int32_t *)(s + i);
196- int32_t v1 = safefetch32_impl (aligned, SENT_A );
197- int32_t v2 = safefetch32_impl (aligned, SENT_B );
198- if (v1 == SENT_A && v2 == SENT_B ) {
199- return false ;
200- }
201- size_t chunk = (len - i >= 4 ) ? 4 : (len - i);
202- memcpy (d + i, &v1, chunk); // memcpy from local — no UAF risk
203- i += chunk;
204- }
205- return true ;
268+ // The copy runs entirely inside the safecopy_impl assembly stub, which
269+ // reads `src` one byte at a time. If a load faults, handle_safefetch
270+ // redirects execution to safecopy_cont, which returns 0. Because the copy
271+ // is byte-granular it only ever touches bytes in [src, src+len) — there is
272+ // no over-read past the requested range and no alignment reasoning needed.
273+ // A fault mid-copy may leave up to len-1 bytes already written to `dst`.
274+ return safecopy_impl (dst, src, len) != 0 ;
206275}
207276
208277bool SafeAccess::handle_safefetch (int sig, void * context) {
@@ -215,6 +284,11 @@ bool SafeAccess::handle_safefetch(int sig, void* context) {
215284 } else if (pc == (uintptr_t )safefetch64_impl) {
216285 uc->current_pc = (uintptr_t )safefetch64_cont;
217286 return true ;
287+ } else if (pc >= (uintptr_t )safecopy_impl && pc < (uintptr_t )safecopy_cont) {
288+ // Unlike safefetch, the faulting load can be at any pc inside the copy
289+ // loop, so match the whole [safecopy_impl, safecopy_cont) range.
290+ uc->current_pc = (uintptr_t )safecopy_cont;
291+ return true ;
218292 }
219293 }
220294 return false ;
0 commit comments