1 //! Integration tests for guest-debug support (gdbstub + LLDB).
2 //!
3 //! These tests launch `wasmtime run` with `-g <port>`, connect LLDB via
4 //! the wasm remote protocol, execute debug scripts (set breakpoints,
5 //! continue, step, inspect variables), and validate output.
6 //!
7 //! Requirements:
8 //!   - LLDB with Wasm plugin support (`LLDB` env var or `/opt/wasi-sdk/bin/lldb` by default)
9 //!   - `WASI_SDK_PATH` env var set (for C test programs)
10 //!   - Built with `--features gdbstub`
11 
12 use filecheck::{CheckerBuilder, NO_VARIABLES};
13 use std::io::{BufRead, BufReader, Write};
14 use std::net::{SocketAddr, TcpListener, TcpStream};
15 use std::process::{Child, Command, Stdio};
16 use std::time::Duration;
17 use test_programs_artifacts::*;
18 use wasmtime::{Result, bail, format_err};
19 
20 /// Find the wasmtime binary built alongside the test binary.
wasmtime_binary() -> std::path::PathBuf21 fn wasmtime_binary() -> std::path::PathBuf {
22     let mut me = std::env::current_exe().expect("current_exe specified");
23     me.pop(); // chop off file name
24     me.pop(); // chop off `deps`
25     if cfg!(target_os = "windows") {
26         me.push("wasmtime.exe");
27     } else {
28         me.push("wasmtime");
29     }
30     me
31 }
32 
33 /// Find an available TCP port by binding to port 0.
free_port() -> u1634 fn free_port() -> u16 {
35     TcpListener::bind("127.0.0.1:0")
36         .unwrap()
37         .local_addr()
38         .unwrap()
39         .port()
40 }
41 
42 /// Path to the wasm-aware LLDB.
lldb_path() -> String43 fn lldb_path() -> String {
44     std::env::var("LLDB").unwrap_or("/opt/wasi-sdk/bin/lldb".to_string())
45 }
46 
47 /// The readiness marker printed by the gdbstub to stderr.
48 const GDBSTUB_READY_MARKER: &str = "Debugger listening on";
49 
50 /// A running wasmtime process with a gdbstub endpoint.
51 struct WasmtimeWithGdbstub {
52     child: Child,
53     /// Keeps the stderr pipe alive to avoid SIGPIPE on the child.
54     /// Also used by serve tests to read the HTTP address.
55     stderr_reader: BufReader<std::process::ChildStderr>,
56 }
57 
58 impl WasmtimeWithGdbstub {
59     /// Spawn wasmtime and wait for stderr to contain the gdbstub
60     /// readiness marker.
spawn( subcmd: &str, gdbstub_port: u16, extra_args: &[&str], timeout: Duration, ) -> Result<Self>61     fn spawn(
62         subcmd: &str,
63         gdbstub_port: u16,
64         extra_args: &[&str],
65         timeout: Duration,
66     ) -> Result<Self> {
67         let mut cmd = Command::new(wasmtime_binary());
68         cmd.arg(subcmd)
69             .arg(format!("-g{gdbstub_port}"))
70             .args(extra_args)
71             .stdin(Stdio::null())
72             .stdout(Stdio::null())
73             .stderr(Stdio::piped());
74         eprintln!("spawning: {cmd:?}");
75         let mut child = cmd.spawn()?;
76 
77         let stderr = child.stderr.take().unwrap();
78         let mut reader = BufReader::new(stderr);
79         let deadline = std::time::Instant::now() + timeout;
80         let mut line = String::new();
81         loop {
82             if std::time::Instant::now() > deadline {
83                 let _ = child.kill();
84                 bail!("timed out waiting for gdbstub readiness");
85             }
86             line.clear();
87             reader.read_line(&mut line)?;
88             eprintln!("wasmtime stderr: {}", line.trim_end());
89             if line.contains(GDBSTUB_READY_MARKER) {
90                 return Ok(Self {
91                     child,
92                     stderr_reader: reader,
93                 });
94             }
95             if line.is_empty() {
96                 let _ = child.kill();
97                 let status = child.wait()?;
98                 bail!("wasmtime exited ({status}) without readiness marker");
99             }
100         }
101     }
102 
103     /// Read stderr lines until one contains `marker`, returning that line.
wait_for_stderr(&mut self, marker: &str, timeout: Duration) -> Result<String>104     fn wait_for_stderr(&mut self, marker: &str, timeout: Duration) -> Result<String> {
105         let deadline = std::time::Instant::now() + timeout;
106         let mut line = String::new();
107         loop {
108             if std::time::Instant::now() > deadline {
109                 bail!("timed out waiting for '{marker}' on stderr");
110             }
111             line.clear();
112             self.stderr_reader.read_line(&mut line)?;
113             eprintln!("wasmtime stderr: {}", line.trim_end());
114             if line.contains(marker) {
115                 return Ok(line);
116             }
117             if line.is_empty() {
118                 bail!("wasmtime stderr closed before finding '{marker}'");
119             }
120         }
121     }
122 }
123 
124 /// Run an LLDB debug script against a gdbstub endpoint.
125 ///
126 /// Connects LLDB to `127.0.0.1:<port>` using the Wasm plugin,
127 /// executes the given script commands, and returns LLDB's stdout.
lldb_with_gdbstub_script(port: u16, script: &str) -> Result<String>128 fn lldb_with_gdbstub_script(port: u16, script: &str) -> Result<String> {
129     let _ = env_logger::try_init();
130 
131     let mut cmd = Command::new(lldb_path());
132     cmd.arg("--batch");
133     cmd.arg("-o").arg(format!(
134         "process connect --plugin wasm connect://127.0.0.1:{port}"
135     ));
136     for line in script.lines() {
137         let line = line.trim();
138         if !line.is_empty() {
139             cmd.arg("-o").arg(line);
140         }
141     }
142 
143     eprintln!("Running LLDB: {cmd:?}");
144     let output = cmd.output()?;
145 
146     let stdout = String::from_utf8(output.stdout)?;
147     let stderr = String::from_utf8(output.stderr)?;
148     eprintln!("--- LLDB stdout ---\n{stdout}");
149     eprintln!("--- LLDB stderr ---\n{stderr}");
150 
151     Ok(stdout)
152 }
153 
154 /// Validate output against FileCheck-style directives.
check_output(output: &str, directives: &str) -> Result<()>155 fn check_output(output: &str, directives: &str) -> Result<()> {
156     let mut builder = CheckerBuilder::new();
157     builder
158         .text(directives)
159         .map_err(|e| format_err!("unable to build checker: {e:?}"))?;
160     let checker = builder.finish();
161     let check = checker
162         .explain(output, NO_VARIABLES)
163         .map_err(|e| format_err!("{e:?}"))?;
164     assert!(check.0, "didn't pass check {}", check.1);
165     Ok(())
166 }
167 
168 /// Test that breakpoints can be set at the initial stop (before any
169 /// continue), then hit when the program runs.
170 #[test]
171 #[ignore]
guest_debug_cli_fib_breakpoint() -> Result<()>172 fn guest_debug_cli_fib_breakpoint() -> Result<()> {
173     let port = free_port();
174     let mut wt = WasmtimeWithGdbstub::spawn(
175         "run",
176         port,
177         &["-Ccache=n", GUEST_DEBUG_FIB],
178         Duration::from_secs(30),
179     )?;
180 
181     // Set breakpoint at the initial stop, *before* continuing.
182     // This tests that modules are visible immediately.
183     let output = lldb_with_gdbstub_script(
184         port,
185         r#"
186 b fib
187 c
188 fr v
189 c
190 "#,
191     )?;
192     wt.child.kill().ok();
193     wt.child.wait()?;
194 
195     check_output(
196         &output,
197         r#"
198 check: stop reason
199 check: fib
200 check: n =
201 "#,
202     )?;
203     Ok(())
204 }
205 
206 /// Test single-stepping within fib.
207 #[test]
208 #[ignore]
guest_debug_cli_fib_step() -> Result<()>209 fn guest_debug_cli_fib_step() -> Result<()> {
210     let port = free_port();
211     let mut wt = WasmtimeWithGdbstub::spawn(
212         "run",
213         port,
214         &["-Ccache=n", GUEST_DEBUG_FIB],
215         Duration::from_secs(30),
216     )?;
217 
218     let output = lldb_with_gdbstub_script(
219         port,
220         r#"
221 b fib
222 c
223 n
224 n
225 n
226 fr v
227 c
228 "#,
229     )?;
230     wt.child.kill().ok();
231     wt.child.wait()?;
232 
233     check_output(
234         &output,
235         r#"
236 check: stop reason
237 check: fib
238 "#,
239     )?;
240     Ok(())
241 }
242 
243 /// Helper: send an HTTP/1.0 request and return the full response.
http_request(addr: SocketAddr, path: &str) -> Result<String>244 fn http_request(addr: SocketAddr, path: &str) -> Result<String> {
245     let mut tcp = TcpStream::connect_timeout(&addr, Duration::from_secs(5))?;
246     tcp.set_read_timeout(Some(Duration::from_secs(5)))?;
247     write!(tcp, "GET {path} HTTP/1.0\r\nHost: localhost\r\n\r\n")?;
248     let mut response = String::new();
249     let _ = std::io::Read::read_to_string(&mut tcp, &mut response);
250     Ok(response)
251 }
252 
253 /// Parse an HTTP serve address from a "Serving HTTP on http://addr/" line.
parse_http_addr(line: &str) -> Result<SocketAddr>254 fn parse_http_addr(line: &str) -> Result<SocketAddr> {
255     line.find("127.0.0.1")
256         .and_then(|start| {
257             let addr = &line[start..];
258             let end = addr.find('/')?;
259             addr[..end].parse().ok()
260         })
261         .ok_or_else(|| format_err!("failed to parse HTTP address from: {line}"))
262 }
263 
264 /// Start serve under debugger, continue, and send multiple HTTP requests
265 /// to verify instance reuse works correctly under the debugger.
266 #[test]
267 #[ignore]
guest_debug_serve_requests() -> Result<()>268 fn guest_debug_serve_requests() -> Result<()> {
269     let gdb_port = free_port();
270 
271     let mut wt = WasmtimeWithGdbstub::spawn(
272         "serve",
273         gdb_port,
274         &[
275             "-Ccache=n",
276             "--addr=127.0.0.1:0",
277             "-Scli",
278             P2_CLI_SERVE_HELLO_WORLD_COMPONENT,
279         ],
280         Duration::from_secs(30),
281     )?;
282 
283     // Connect LLDB in background: just continue to start the HTTP server.
284     let lldb_handle = std::thread::spawn(move || lldb_with_gdbstub_script(gdb_port, "c\n"));
285 
286     // Wait for the HTTP server to start.
287     let line = wt.wait_for_stderr("Serving HTTP", Duration::from_secs(15))?;
288     let http_addr = parse_http_addr(&line)?;
289     eprintln!("HTTP address: {http_addr}");
290 
291     // Send 3 requests to the same instance, verifying instance reuse.
292     for i in 1..=3 {
293         let resp = http_request(http_addr, "/")?;
294         eprintln!("Response {i}: {}", resp.lines().last().unwrap_or(""));
295         assert!(
296             resp.contains("Hello, WASI!"),
297             "request {i}: expected 'Hello, WASI!' in response, got:\n{resp}"
298         );
299     }
300 
301     // Kill wasmtime to unblock LLDB (which is waiting for the process).
302     wt.child.kill().ok();
303     wt.child.wait()?;
304 
305     // Collect LLDB output (it exits once the process is killed).
306     let lldb_output = lldb_handle.join().unwrap()?;
307 
308     // Verify LLDB connected and the process was running.
309     check_output(
310         &lldb_output,
311         r#"
312 check: stop reason
313 check: resuming
314 "#,
315     )?;
316 
317     Ok(())
318 }
319 
320 /// Start serve under debugger, set a breakpoint on the HTTP handler,
321 /// send requests, verify breakpoints fire and responses are correct.
322 /// Tests instance reuse across multiple requests.
323 #[test]
324 #[ignore]
guest_debug_serve_breakpoint() -> Result<()>325 fn guest_debug_serve_breakpoint() -> Result<()> {
326     let gdb_port = free_port();
327 
328     let mut wt = WasmtimeWithGdbstub::spawn(
329         "serve",
330         gdb_port,
331         &[
332             "-Ccache=n",
333             "--addr=127.0.0.1:0",
334             "-Scli",
335             P2_CLI_SERVE_HELLO_WORLD_COMPONENT,
336         ],
337         Duration::from_secs(30),
338     )?;
339 
340     // LLDB script: set a breakpoint on the incoming-handler Guest::handle,
341     // continue to start the server, then for each request: print backtrace
342     // at breakpoint and continue. We do this for 3 requests.
343     let lldb_handle = std::thread::spawn(move || {
344         lldb_with_gdbstub_script(
345             gdb_port,
346             r#"
347 rbreak Guest.*handle
348 c
349 bt
350 c
351 bt
352 c
353 bt
354 c
355 "#,
356         )
357     });
358 
359     // Wait for the HTTP server to start.
360     let line = wt.wait_for_stderr("Serving HTTP", Duration::from_secs(15))?;
361     let http_addr = parse_http_addr(&line)?;
362     eprintln!("HTTP address: {http_addr}");
363 
364     // Send 3 requests. Each one will hit the breakpoint, LLDB prints
365     // the backtrace, then continues to let the response through.
366     for i in 1..=3 {
367         let resp = http_request(http_addr, "/")?;
368         eprintln!("Response {i}: {}", resp.lines().last().unwrap_or(""));
369         assert!(
370             resp.contains("Hello, WASI!"),
371             "request {i}: expected 'Hello, WASI!' in response, got:\n{resp}"
372         );
373     }
374 
375     // Kill wasmtime to unblock LLDB.
376     wt.child.kill().ok();
377     wt.child.wait()?;
378 
379     let lldb_output = lldb_handle.join().unwrap()?;
380 
381     // Verify LLDB stopped at the breakpoint with the correct function
382     // in the backtrace, and that it happened multiple times.
383     check_output(
384         &lldb_output,
385         r#"
386 check: Guest
387 check: handle
388 check: stop reason
389 check: Guest
390 check: handle
391 check: stop reason
392 check: Guest
393 check: handle
394 "#,
395     )?;
396 
397     Ok(())
398 }
399