Skip to content

Commit ec57fa1

Browse files
committed
Optimize SafeAccess:safeCopy() implementation in assembly
1 parent 94bf8cb commit ec57fa1

3 files changed

Lines changed: 115 additions & 64 deletions

File tree

ddprof-lib/src/main/cpp/safeAccess.cpp

Lines changed: 100 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222
extern "C" int safefetch32_cont(int* adr, int errValue);
2323
extern "C" int64_t safefetch64_cont(int64_t* adr, int64_t errValue);
2424

25+
// safecopy_impl copies `len` bytes from `src` to `dst` one byte at a time and
26+
// returns 1 on success. If any load from `src` faults, the signal handler
27+
// redirects the pc anywhere in [safecopy_impl, safecopy_cont) to safecopy_cont,
28+
// which returns 0. Both are leaf routines with no stack frame, so the redirect
29+
// keeps the return address intact.
30+
extern "C" int safecopy_impl(void* dst, const void* src, size_t len);
31+
extern "C" int safecopy_cont(void* dst, const void* src, size_t len);
32+
2533
#ifdef __APPLE__
2634
#if defined(__x86_64__)
2735
#define current_pc context_rip
@@ -67,6 +75,25 @@ extern "C" int64_t safefetch64_cont(int64_t* adr, int64_t errValue);
6775
_safefetch64_cont:
6876
movq %rsi, %rax
6977
ret
78+
.globl _safecopy_impl
79+
.private_extern _safecopy_impl
80+
_safecopy_impl:
81+
xorl %ecx, %ecx
82+
Lsafecopy_loop:
83+
cmpq %rdx, %rcx
84+
jae Lsafecopy_done
85+
movzbl (%rsi,%rcx,1), %eax
86+
movb %al, (%rdi,%rcx,1)
87+
incq %rcx
88+
jmp Lsafecopy_loop
89+
Lsafecopy_done:
90+
movl $1, %eax
91+
ret
92+
.globl _safecopy_cont
93+
.private_extern _safecopy_cont
94+
_safecopy_cont:
95+
xorl %eax, %eax
96+
ret
7097
)");
7198
#else
7299
asm(R"(
@@ -95,6 +122,27 @@ extern "C" int64_t safefetch64_cont(int64_t* adr, int64_t errValue);
95122
safefetch64_cont:
96123
movq %rsi, %rax
97124
ret
125+
.globl safecopy_impl
126+
.hidden safecopy_impl
127+
.type safecopy_impl, %function
128+
safecopy_impl:
129+
xorl %ecx, %ecx
130+
.Lsafecopy_loop:
131+
cmpq %rdx, %rcx
132+
jae .Lsafecopy_done
133+
movzbl (%rsi,%rcx,1), %eax
134+
movb %al, (%rdi,%rcx,1)
135+
incq %rcx
136+
jmp .Lsafecopy_loop
137+
.Lsafecopy_done:
138+
movl $1, %eax
139+
ret
140+
.globl safecopy_cont
141+
.hidden safecopy_cont
142+
.type safecopy_cont, %function
143+
safecopy_cont:
144+
xorl %eax, %eax
145+
ret
98146
)");
99147
#endif // __APPLE__
100148
#elif defined(__aarch64__)
@@ -120,6 +168,25 @@ extern "C" int64_t safefetch64_cont(int64_t* adr, int64_t errValue);
120168
_safefetch64_cont:
121169
mov x0, x1
122170
ret
171+
.globl _safecopy_impl
172+
.private_extern _safecopy_impl
173+
_safecopy_impl:
174+
mov x3, xzr
175+
Lsafecopy_loop:
176+
cmp x3, x2
177+
b.hs Lsafecopy_done
178+
ldrb w4, [x1, x3]
179+
strb w4, [x0, x3]
180+
add x3, x3, #1
181+
b Lsafecopy_loop
182+
Lsafecopy_done:
183+
mov w0, #1
184+
ret
185+
.globl _safecopy_cont
186+
.private_extern _safecopy_cont
187+
_safecopy_cont:
188+
mov w0, #0
189+
ret
123190
)");
124191
#else
125192
asm(R"(
@@ -148,61 +215,39 @@ extern "C" int64_t safefetch64_cont(int64_t* adr, int64_t errValue);
148215
safefetch64_cont:
149216
mov x0, x1
150217
ret
218+
.globl safecopy_impl
219+
.hidden safecopy_impl
220+
.type safecopy_impl, %function
221+
safecopy_impl:
222+
mov x3, xzr
223+
.Lsafecopy_loop:
224+
cmp x3, x2
225+
b.hs .Lsafecopy_done
226+
ldrb w4, [x1, x3]
227+
strb w4, [x0, x3]
228+
add x3, x3, #1
229+
b .Lsafecopy_loop
230+
.Lsafecopy_done:
231+
mov w0, #1
232+
ret
233+
.globl safecopy_cont
234+
.hidden safecopy_cont
235+
.type safecopy_cont, %function
236+
safecopy_cont:
237+
mov w0, #0
238+
ret
151239
)");
152240
#endif
153241
#endif
154242

155243
bool 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;
244+
// The copy runs entirely inside the safecopy_impl assembly stub, which
245+
// reads `src` one byte at a time. If a load faults, handle_safefetch
246+
// redirects execution to safecopy_cont, which returns 0. Because the copy
247+
// is byte-granular it only ever touches bytes in [src, src+len) — there is
248+
// no over-read past the requested range and no alignment reasoning needed.
249+
// A fault mid-copy may leave up to len-1 bytes already written to `dst`.
250+
return safecopy_impl(dst, src, len) != 0;
206251
}
207252

208253
bool SafeAccess::handle_safefetch(int sig, void* context) {
@@ -215,6 +260,11 @@ bool SafeAccess::handle_safefetch(int sig, void* context) {
215260
} else if (pc == (uintptr_t)safefetch64_impl) {
216261
uc->current_pc = (uintptr_t)safefetch64_cont;
217262
return true;
263+
} else if (pc >= (uintptr_t)safecopy_impl && pc < (uintptr_t)safecopy_cont) {
264+
// Unlike safefetch, the faulting load can be at any pc inside the copy
265+
// loop, so match the whole [safecopy_impl, safecopy_cont) range.
266+
uc->current_pc = (uintptr_t)safecopy_cont;
267+
return true;
218268
}
219269
}
220270
return false;

ddprof-lib/src/main/cpp/safeAccess.h

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,12 @@ class SafeAccess {
6464
return safefetch64_impl(ptr, errorValue);
6565
}
6666

67-
// Copies up to len bytes from src to dst using safefetch32_impl so that a
68-
// page-unmap or repurpose of src memory during the copy does not crash the
69-
// process. Returns true on full success, false if any read faulted. dst must
70-
// have at least len bytes capacity; reads from src may over-read up to 3
71-
// bytes past src+len (over-read is also safefetch-protected).
67+
// Copies len bytes from src to dst via the safecopy_impl assembly stub so
68+
// that a page-unmap or repurpose of src memory during the copy does not
69+
// crash the process. Returns true on full success, false if any read
70+
// faulted (in which case dst may hold a partial prefix). dst must have at
71+
// least len bytes capacity. The copy is byte-granular, so it never reads
72+
// past src+len.
7273
NOINLINE
7374
static bool safeCopy(void* dst, const void* src, size_t len);
7475

ddprof-lib/src/test/cpp/safefetch_ut.cpp

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -162,16 +162,16 @@ TEST_F(SafeFetchTest, mprotectedMemory64) {
162162

163163
// ---------------------------------------------------------------------------
164164
// SafeAccess::safeCopy — bulk variant of safeFetch{32,64} that copies a byte
165-
// range via the safefetch trampoline. Must:
165+
// range via the safecopy_impl assembly stub (byte-granular loads guarded by
166+
// handle_safefetch). Must:
166167
// - return true and copy the bytes exactly when src is fully readable,
167168
// including when [src, src+len) sits within a few bytes of an unmapped
168-
// page boundary (aligned-load strategy keeps over-reads in-page)
169+
// page boundary (byte-granular reads never over-read past src+len)
169170
// - return false (no crash) when the requested range itself crosses into
170171
// an unmapped page
171-
// - handle unaligned src by fetching at the previous 4-byte aligned
172-
// address and discarding the leading 1..3 bytes
173-
// - never write past dst[len-1] even when len is not a multiple of 4
174-
// - not mis-classify real data as a fault when it equals one sentinel
172+
// - handle unaligned src (no alignment requirement on src)
173+
// - never write past dst[len-1] regardless of len
174+
// - copy real data faithfully regardless of its byte values
175175
// ---------------------------------------------------------------------------
176176

177177
TEST_F(SafeFetchTest, safeCopy_happyPath) {
@@ -188,7 +188,7 @@ TEST_F(SafeFetchTest, safeCopy_zeroLength) {
188188
}
189189

190190
TEST_F(SafeFetchTest, safeCopy_shortLength_doesNotOverwriteDst) {
191-
// The internal 4-byte fetch must not overflow dst beyond len bytes.
191+
// The copy must not write beyond len bytes into dst.
192192
const char src[] = "AB";
193193
char dst[8];
194194
memset(dst, 0x5A, sizeof(dst));
@@ -329,8 +329,8 @@ TEST_F(SafeFetchTest, safeCopy_unalignedShortAtPageEnd_stillSucceeds) {
329329
}
330330

331331
TEST_F(SafeFetchTest, safeCopy_dataMatchingSingleSentinel_stillSucceeds) {
332-
// The two-sentinel pattern must not mis-classify real data that happens
333-
// to equal one of the sentinels. SENT_A is 0x55AA55AA.
332+
// Real data is copied faithfully regardless of its byte values (the copy
333+
// no longer uses sentinel values to detect faults).
334334
uint32_t real_data = 0x55AA55AA;
335335
char dst[4];
336336
ASSERT_TRUE(SafeAccess::safeCopy(dst, &real_data, 4));

0 commit comments

Comments
 (0)