Skip to content

Commit d1b0295

Browse files
committed
Implement c-shared go detours for go 1.24 amd64
1 parent 599c6fe commit d1b0295

3 files changed

Lines changed: 242 additions & 0 deletions

File tree

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
2+
pub(crate) mod go_1_24 {
3+
use std::arch::naked_asm;
4+
5+
use crate::{hooks::HookManager, macros::hook_symbol};
6+
7+
pub(crate) fn hook(hook_manager: &mut HookManager, module_name: &str) {
8+
hook_symbol!(
9+
hook_manager,
10+
module_name,
11+
"internal/runtime/syscall.Syscall6",
12+
internal_runtime_syscall_syscall6_detour
13+
);
14+
}
15+
16+
/// Detour of `internal/runtime/syscall.Syscall6`.
17+
///
18+
/// func Syscall6(num, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, errno uintptr)
19+
/// https://github.com/golang/go/blob/go1.24.11/src/internal/runtime/syscall/asm_linux_amd64.s#L27
20+
///
21+
/// Function arguments are mapped as the following:
22+
/// rax = num
23+
/// rbx = a1
24+
/// rcx = a2
25+
/// rdi = a3
26+
/// rsi = a4
27+
/// r8 = a5
28+
/// r9 = a6
29+
/// r14 = g
30+
#[unsafe(naked)]
31+
unsafe extern "C" fn internal_runtime_syscall_syscall6_detour() {
32+
naked_asm!(
33+
// If the syscall is SYS_EXIT or SYS_EXIT_GROUP,
34+
// skip our logic and just execute it.
35+
"cmp rax,60", // SYS_EXIT
36+
"je 2f",
37+
"cmp rax,231", // SYS_EXIT_GROUP
38+
"je 2f",
39+
"push rbp",
40+
"mov rbp,rsp",
41+
// Reservce space and store args.
42+
"sub rsp,0x40",
43+
"mov [rsp+0x38],r9",
44+
"mov [rsp+0x30],r8",
45+
"mov [rsp+0x28],rsi",
46+
"mov [rsp+0x20],rdi",
47+
"mov [rsp+0x18],rcx",
48+
"mov [rsp+0x10],rbx",
49+
"mov [rsp],rax",
50+
// function args starting at rdi.
51+
"mov rdi,rsp",
52+
"call {c_abi_wrapper_on_systemstack}",
53+
// Check failure
54+
"cmp rax,-0xfff",
55+
"jbe 1f",
56+
// Syscall failed.
57+
// Save errno in rcx.
58+
"neg rax",
59+
"mov rcx,rax",
60+
// Fill -1 in rax.
61+
"mov rax,-0x1",
62+
// Clear result register.
63+
"mov rbx,0x0",
64+
// Drop space reserved for locals.
65+
"add rsp,0x40",
66+
// Restore rbp and return.
67+
"pop rbp",
68+
"ret",
69+
// Syscall did not fail. Result is in rax. Clear other result registers.
70+
"1:",
71+
"mov rbx,0x0",
72+
"mov rcx,0x0",
73+
// Drop space for storing args locally.
74+
"add rsp,0x40",
75+
// Restore rbp and return.
76+
"pop rbp",
77+
"ret",
78+
// Move the first syscall argument to the correct register (rdx),
79+
// and execute the syscall.
80+
"2:",
81+
"mov rdx,rdi",
82+
"syscall",
83+
84+
c_abi_wrapper_on_systemstack = sym c_abi_wrapper_on_systemstack,
85+
);
86+
}
87+
88+
/// Calls [`c_abi_wrapper`] on systemstack.
89+
///
90+
/// Implemented based on [`runtime.systemstack`](https://github.com/golang/go/blob/go1.24.11/src/runtime/asm_amd64.s#L483)
91+
#[unsafe(naked)]
92+
unsafe extern "C" fn c_abi_wrapper_on_systemstack() {
93+
naked_asm!(
94+
// This is required, as we call `gosave_systemstack_switch` later.
95+
// Not having any local variables in this function is required as well.
96+
"push rbp",
97+
"mov rbp,rsp",
98+
// We assume that r14 stores the address of g.
99+
// Load address of current m to rbx.
100+
// https://github.com/golang/go/blob/go1.24.11/src/runtime/runtime2.go#L410
101+
"mov rbx,[r14+0x30]",
102+
// Check if g is m->gsignal. If so, do not switch stack.
103+
// https://github.com/golang/go/blob/go1.24.11/src/runtime/runtime2.go#L536
104+
"cmp r14,[rbx+0x50]",
105+
"je 1f",
106+
// Load address of m->g0 to rdx.
107+
// Check if g is g0. If so, do not switch stack.
108+
"mov rdx,[rbx]",
109+
"cmp r14,rdx",
110+
"je 1f",
111+
// We expect g is m->curg now. If not, abort with `ud2`.
112+
// https://github.com/golang/go/blob/go1.24.11/src/runtime/runtime2.go#L541
113+
"cmp r14,[rbx+0xc0]",
114+
"jne 2f",
115+
// Switch to system stack.
116+
"call {gosave_systemstack_switch}",
117+
// rdx holds the address of m->g0. Store it also in TLS and r14.
118+
"mov fs:0xfffffffffffffff8,rdx",
119+
"mov r14,rdx",
120+
// Fill rsp with g0's g->sched->sp. After this, we are on system stack.
121+
"mov rsp,[rdx+0x38]",
122+
// We assume rdi still has the original syscall args stored on user g stack.
123+
"call {c_abi_wrapper}",
124+
// Switch back to g stack.
125+
// https://github.com/golang/go/blob/go1.24.11/src/runtime/asm_amd64.s#L516-L526
126+
// We assume r14 still has g0. Store g0's m in rbx.
127+
"mov rbx,[r14+0x30]",
128+
// Store m->curg in rsi.
129+
"mov rsi,[rbx+0xc0]",
130+
// Store user g in TLS
131+
"mov fs:0xfffffffffffffff8,rsi",
132+
// Store user g in r14
133+
"mov r14,rsi",
134+
// Fill rsp with g->sched->sp
135+
"mov rsp,[rsi+0x38]",
136+
// Fill rbp with g->sched->bp
137+
// https://github.com/golang/go/blob/go1.24.11/src/runtime/runtime2.go#L317
138+
"mov rbp,[rsi+0x68]",
139+
"mov QWORD PTR [rsi+0x38],0x0",
140+
"mov QWORD PTR [rsi+0x68],0x0",
141+
// Restore rbp and return.
142+
"pop rbp",
143+
"ret",
144+
// Already on system stack. Tail call `c_abi_wrapper`.
145+
// https://github.com/golang/go/blob/go1.24.11/src/runtime/asm_amd64.s#L528-L537
146+
"1:",
147+
"pop rbp",
148+
"jmp {c_abi_wrapper}",
149+
// Abort the program.
150+
"2:",
151+
"ud2",
152+
gosave_systemstack_switch = sym gosave_systemstack_switch,
153+
c_abi_wrapper = sym c_abi_wrapper,
154+
);
155+
}
156+
157+
/// Exact copy of the `go_1_25::c_abi_wrapper` function.
158+
///
159+
/// Move function args from stack to registers so we can call
160+
/// [`c_abi_syscall6_handler`](crate::go::c_abi_syscall6_handler).
161+
///
162+
/// We expect rdi stores the address of the start of function args on the stack.
163+
///
164+
/// C ABI: fn(rdi, rsi, rdx, rcx, r8, r9, [stack])
165+
#[unsafe(naked)]
166+
unsafe extern "C" fn c_abi_wrapper() {
167+
naked_asm!(
168+
"push rbp",
169+
"mov rbp,rsp",
170+
"sub rsp,0x8",
171+
"and rsp,-0x10",
172+
"mov rax,[rdi]",
173+
"mov rsi,[rdi+0x10]",
174+
"mov rdx,[rdi+0x18]",
175+
"mov rcx,[rdi+0x20]",
176+
"mov r8,[rdi+0x28]",
177+
"mov r9,[rdi+0x30]",
178+
"mov r10,[rdi+0x38]",
179+
"mov [rsp],r10",
180+
"mov rdi,rax",
181+
"call c_abi_syscall6_handler",
182+
"mov rsp, rbp",
183+
"pop rbp",
184+
"ret"
185+
);
186+
}
187+
188+
/// Implemented based on [`gosave_systemstack_switch`](https://github.com/golang/go/blob/go1.24.11/src/runtime/asm_amd64.s#L823)
189+
///
190+
/// Smashes r9.
191+
#[unsafe(naked)]
192+
unsafe extern "C" fn gosave_systemstack_switch() {
193+
naked_asm!(
194+
// FIXME: In Go's implementation, it loads the address of `runtime.systemstack_switch` + 8 bytes.
195+
// [`runtime.systemstack_switch`](https://github.com/golang/go/blob/go1.24.11/src/runtime/asm_amd64.s#L475)
196+
// is a dummy marker function that shall never be called.
197+
// Go runtime uses it to determine if a g is curently switched to systemstack.
198+
//
199+
// In case of dlopen() multiple c-shared go libs, I don't know how to do it yet :)
200+
"lea r9, [rip + {runtime_abort}]",
201+
// Store r9 in g->sched->pc.
202+
"mov QWORD PTR [r14+0x40],r9",
203+
"lea r9, [rsp+0x8]",
204+
// Store r9 in g->sched->sp.
205+
"mov QWORD PTR [r14+0x38],r9",
206+
// Store r9 in g->sched->ret.
207+
"mov QWORD PTR [r14+0x58],0x0",
208+
// Store r9 in g->sched->bp.
209+
"mov QWORD PTR [r14+0x68],rbp",
210+
// Store g->sched->ctxt in r9.
211+
"mov r9, QWORD PTR [r14+0x50]",
212+
// If g->sched->ctxt == 0 abort runtime, otherwise return.
213+
"test r9, r9",
214+
"jz 1f",
215+
"call {runtime_abort}",
216+
"1:",
217+
"ret",
218+
219+
runtime_abort = sym runtime_abort,
220+
);
221+
}
222+
223+
/// Implemented based on [`runtime.abort`](https://github.com/golang/go/blob/fed3b0a298464457c58d1150bdb3942f22bd6220/src/runtime/asm_amd64.s#L1237)
224+
///
225+
/// This function crashes the runtime. `int3` is recognized by debuggers.
226+
/// The rest of the function is an infinite trap loop.
227+
#[unsafe(naked)]
228+
unsafe extern "C" fn runtime_abort() {
229+
naked_asm!("int3", "1:", "jmp 1b",);
230+
}
231+
}

mirrord/layer/src/go/linux_x64.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,8 +640,14 @@ pub(crate) fn enable_hooks_in_loaded_module(hook_manager: &mut HookManager, modu
640640
return;
641641
};
642642

643+
tracing::trace!(version, module_name, "Detected Go");
643644
if version >= 1.25 {
644645
go_1_25::hook_in_module(hook_manager, module_name.as_str());
646+
} else if version >= 1.24 {
647+
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
648+
crate::go::c_shared::go_1_24::hook(hook_manager, module_name.as_str());
649+
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
650+
post_go1_23(hook_manager, Some(module_name.as_str()));
645651
} else if version >= 1.23 {
646652
post_go1_23(hook_manager, Some(module_name.as_str()));
647653
} else if version >= 1.19 {

mirrord/layer/src/go/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ use tracing::trace;
1010

1111
use crate::{close_detour, file::hooks::*, hooks::HookManager, socket::hooks::*};
1212

13+
#[cfg(all(
14+
any(target_arch = "x86_64", target_arch = "aarch64"),
15+
target_os = "linux"
16+
))]
17+
pub(crate) mod c_shared;
1318
#[cfg_attr(
1419
all(target_os = "linux", target_arch = "x86_64"),
1520
path = "linux_x64.rs"

0 commit comments

Comments
 (0)