diff --git a/Cargo.lock b/Cargo.lock index 34b71018778c..ed30f6b51fc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anstream" version = "0.6.21" @@ -147,6 +156,12 @@ dependencies = [ "arbitrary", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -164,6 +179,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -335,7 +361,7 @@ dependencies = [ name = "calculator" version = "0.1.0" dependencies = [ - "clap", + "clap 4.5.48", "wasmtime", "wasmtime-wasi", ] @@ -551,6 +577,21 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap", + "unicode-width 0.1.9", + "vec_map", +] + [[package]] name = "clap" version = "4.5.48" @@ -570,7 +611,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.11.1", "terminal_size", ] @@ -580,7 +621,7 @@ version = "4.5.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75bf0b32ad2e152de789bb635ea4d3078f6b838ad7974143e99b99f45a04af4a" dependencies = [ - "clap", + "clap 4.5.48", ] [[package]] @@ -967,7 +1008,7 @@ dependencies = [ name = "cranelift-serde" version = "0.131.0" dependencies = [ - "clap", + "clap 4.5.48", "cranelift-codegen", "cranelift-reader", "serde_json", @@ -984,7 +1025,7 @@ dependencies = [ "anyhow", "capstone", "cfg-if", - "clap", + "clap 4.5.48", "cranelift", "cranelift-codegen", "cranelift-entity", @@ -1029,7 +1070,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap", + "clap 4.5.48", "criterion-plot", "itertools 0.13.0", "num-traits", @@ -1345,6 +1386,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1536,6 +1586,21 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -1591,6 +1656,30 @@ dependencies = [ "serde_json", ] +[[package]] +name = "gdbstub" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bafc7e33650ab9f05dcc16325f05d56b8d10393114e31a19a353b86fa60cfe7" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "log", + "managed", + "num-traits", + "pastey", +] + +[[package]] +name = "gdbstub_arch" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c02bfe7bd65f42bcda751456869dfa1eb2bd1c36e309b9ec27f4888d41cf258" +dependencies = [ + "gdbstub", + "num-traits", +] + [[package]] name = "generic-array" version = "0.14.5" @@ -1723,6 +1812,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "heck" version = "0.4.1" @@ -1735,6 +1833,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -2084,7 +2191,7 @@ version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ - "hermit-abi", + "hermit-abi 0.5.2", "libc", "windows-sys 0.61.2", ] @@ -2109,7 +2216,7 @@ dependencies = [ name = "islec" version = "0.0.0" dependencies = [ - "clap", + "clap 4.5.48", "cranelift-isle", "env_logger 0.11.5", ] @@ -2247,7 +2354,7 @@ checksum = "5297962ef19edda4ce33aaa484386e0a5b3d7f2f4e037cbeee00503ef6b29d33" dependencies = [ "anstream", "anstyle", - "clap", + "clap 4.5.48", "escape8259", ] @@ -2328,6 +2435,12 @@ dependencies = [ "libc", ] +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + [[package]] name = "matchers" version = "0.2.0" @@ -2510,7 +2623,7 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi", + "hermit-abi 0.5.2", "libc", ] @@ -2681,6 +2794,12 @@ dependencies = [ "ureq", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "password-hash" version = "0.4.2" @@ -2698,6 +2817,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +[[package]] +name = "pastey" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec" + [[package]] name = "pbkdf2" version = "0.11.0" @@ -2787,6 +2912,30 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.92", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.101" @@ -2822,7 +2971,7 @@ version = "44.0.0" dependencies = [ "anyhow", "arbitrary", - "clap", + "clap 4.5.48", "cranelift-bitset", "env_logger 0.11.5", "log", @@ -3501,12 +3650,42 @@ dependencies = [ "serde", ] +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "structopt" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +dependencies = [ + "clap 2.34.0", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck 0.3.3", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.92", +] + [[package]] name = "strum" version = "0.24.1" @@ -3627,7 +3806,7 @@ version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ - "fastrand", + "fastrand 2.3.0", "getrandom 0.3.1", "once_cell", "rustix 1.0.8", @@ -3709,6 +3888,15 @@ dependencies = [ "wit-component", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width 0.1.9", +] + [[package]] name = "thiserror" version = "1.0.65" @@ -4144,12 +4332,18 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "veri_engine" version = "0.1.0" dependencies = [ "anyhow", - "clap", + "clap 4.5.48", "cranelift-codegen", "cranelift-codegen-meta", "cranelift-isle", @@ -4190,6 +4384,12 @@ dependencies = [ "libc", ] +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + [[package]] name = "walkdir" version = "2.5.0" @@ -4578,7 +4778,7 @@ name = "wasmtime-bench-api" version = "44.0.0" dependencies = [ "cap-std", - "clap", + "clap 4.5.48", "shuffling-allocator", "target-lexicon", "wasmtime", @@ -4625,7 +4825,7 @@ dependencies = [ "bytesize", "capstone", "cfg-if", - "clap", + "clap 4.5.48", "clap_complete", "cranelift-codegen", "cranelift-filetests", @@ -4674,6 +4874,7 @@ dependencies = [ "wasmtime-internal-cranelift", "wasmtime-internal-debugger", "wasmtime-internal-explorer", + "wasmtime-internal-gdbstub-component-artifact", "wasmtime-internal-unwinder", "wasmtime-test-macros", "wasmtime-test-util", @@ -4696,7 +4897,7 @@ dependencies = [ name = "wasmtime-cli-flags" version = "44.0.0" dependencies = [ - "clap", + "clap 4.5.48", "file-per-thread-logger", "rayon", "serde", @@ -4713,7 +4914,7 @@ dependencies = [ "anyhow", "arbitrary", "arbtest", - "clap", + "clap 4.5.48", "cpp_demangle", "cranelift-bforest", "cranelift-bitset", @@ -4955,6 +5156,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "wasmtime-internal-gdbstub-component" +version = "44.0.0" +dependencies = [ + "anyhow", + "env_logger 0.11.5", + "futures", + "gdbstub", + "gdbstub_arch", + "log", + "structopt", + "wasip2", + "wit-bindgen 0.53.0", + "wstd", +] + +[[package]] +name = "wasmtime-internal-gdbstub-component-artifact" +version = "44.0.0" + [[package]] name = "wasmtime-internal-jit-debug" version = "44.0.0" @@ -5248,7 +5469,7 @@ dependencies = [ name = "wasmtime-wizer" version = "44.0.0" dependencies = [ - "clap", + "clap 4.5.48", "criterion", "env_logger 0.11.5", "log", @@ -5797,6 +6018,38 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "wstd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0903606f1acdecad11576768ecc61ce215d6848652ac16c0e4592bb265e4200e" +dependencies = [ + "anyhow", + "async-task", + "bytes", + "futures-lite", + "http", + "http-body", + "http-body-util", + "itoa", + "pin-project-lite", + "serde", + "serde_json", + "slab", + "wasip2", + "wstd-macro", +] + +[[package]] +name = "wstd-macro" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a9df01a7fb39fbe7e9b5ef76f586f06425dd6f2be350de4781936f72f9899d" +dependencies = [ + "quote", + "syn 2.0.106", +] + [[package]] name = "xattr" version = "1.6.1" diff --git a/Cargo.toml b/Cargo.toml index 8a68c79afcc0..1cf22d043b2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ wasmtime-wasi-http = { workspace = true, optional = true } wasmtime-unwinder = { workspace = true } wasmtime-wizer = { workspace = true, optional = true, features = ['clap', 'wasmtime'] } wasmtime-debugger = { workspace = true, optional = true } +gdbstub-component-artifact = { workspace = true, optional = true } clap = { workspace = true } clap_complete = { workspace = true, optional = true } target-lexicon = { workspace = true } @@ -168,6 +169,8 @@ members = [ "crates/wasi-tls-nativetls", "crates/wasi-tls-openssl", "crates/debugger", + "crates/gdbstub-component", + "crates/gdbstub-component/artifact", "crates/wizer/fuzz", "crates/wizer/tests/regex-test", "crates/wizer/benches/regex-bench", @@ -280,6 +283,7 @@ wasmtime-jit-icache-coherence = { path = "crates/jit-icache-coherence", version wasmtime-wit-bindgen = { path = "crates/wit-bindgen", version = "=44.0.0", package = 'wasmtime-internal-wit-bindgen' } wasmtime-unwinder = { path = "crates/unwinder", version = "=44.0.0", package = 'wasmtime-internal-unwinder' } wasmtime-debugger = { path = "crates/debugger", version = "=44.0.0", package = "wasmtime-internal-debugger" } +gdbstub-component-artifact = { path = "crates/gdbstub-component/artifact", package = "wasmtime-internal-gdbstub-component-artifact" } wasmtime-wizer = { path = "crates/wizer", version = "44.0.0" } # Miscellaneous crates without a `wasmtime-*` prefix in their name but still @@ -358,6 +362,9 @@ wasm-wave = "0.245.0" wasm-compose = "0.245.0" json-from-wast = "0.245.0" +wstd = "0.6.5" +wasip2 = "1.0" + # Non-Bytecode Alliance maintained dependencies: # -------------------------- arbitrary = "1.4.2" @@ -441,6 +448,9 @@ rayon = "1.5.3" regex = "1.9.1" pin-project-lite = "0.2.14" sha2 = { version = "0.10.2", default-features = false } +structopt = "0.3.26" +gdbstub = "0.7.10" +gdbstub_arch = "0.3.3" # ============================================================================= # @@ -566,7 +576,7 @@ gc-drc = ["gc", "wasmtime/gc-drc", "wasmtime-cli-flags/gc-drc"] gc-null = ["gc", "wasmtime/gc-null", "wasmtime-cli-flags/gc-null"] pulley = ["wasmtime-cli-flags/pulley"] stack-switching = ["wasmtime/stack-switching", "wasmtime-cli-flags/stack-switching"] -debug = ["wasmtime-cli-flags/debug", "wasmtime/debug", "component-model"] +debug = ["wasmtime-cli-flags/debug", "wasmtime/debug", "component-model", "dep:gdbstub-component-artifact"] # CLI subcommands for the `wasmtime` executable. See `wasmtime $cmd --help` # for more information on each subcommand. diff --git a/crates/gdbstub-component/Cargo.toml b/crates/gdbstub-component/Cargo.toml new file mode 100644 index 000000000000..1669f3f87715 --- /dev/null +++ b/crates/gdbstub-component/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "wasmtime-internal-gdbstub-component" +version.workspace = true +authors.workspace = true +edition.workspace = true +license = "Apache-2.0 WITH LLVM-exception" +description = "gdbstub debug-component adapter" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wit-bindgen = { workspace = true, features = ["macros"] } +anyhow = { workspace = true } +structopt = { workspace = true } +wstd = { workspace = true } +wasip2 = { workspace = true } +futures = { workspace = true, default-features = true } +gdbstub = { workspace = true } +gdbstub_arch = { workspace = true } +log = { workspace = true } +env_logger = { workspace = true } + +[lints] +workspace = true diff --git a/crates/gdbstub-component/artifact/Cargo.toml b/crates/gdbstub-component/artifact/Cargo.toml new file mode 100644 index 000000000000..45e7c565ace9 --- /dev/null +++ b/crates/gdbstub-component/artifact/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "wasmtime-internal-gdbstub-component-artifact" +version.workspace = true +authors.workspace = true +edition.workspace = true +license = "Apache-2.0 WITH LLVM-exception" +publish = false + +[lints] +workspace = true diff --git a/crates/gdbstub-component/artifact/build.rs b/crates/gdbstub-component/artifact/build.rs new file mode 100644 index 000000000000..0d2d0745dbf5 --- /dev/null +++ b/crates/gdbstub-component/artifact/build.rs @@ -0,0 +1,63 @@ +use std::env; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + + let mut cmd = cargo(); + cmd.arg("build") + .arg("--release") + .arg("--target=wasm32-wasip2") + .arg("--package=wasmtime-internal-gdbstub-component") + .env("CARGO_TARGET_DIR", &out_dir) + .env("RUSTFLAGS", rustflags()) + .env_remove("CARGO_ENCODED_RUSTFLAGS"); + eprintln!("running: {cmd:?}"); + let status = cmd.status().unwrap(); + assert!(status.success()); + + let wasm = out_dir + .join("wasm32-wasip2") + .join("release") + .join("wasmtime_internal_gdbstub_component.wasm"); + + // Read dep-info to get proper rerun-if-changed directives. + let deps_file = wasm.with_extension("d"); + if let Ok(contents) = std::fs::read_to_string(&deps_file) { + for line in contents.lines() { + let Some(pos) = line.find(": ") else { + continue; + }; + let line = &line[pos + 2..]; + let mut parts = line.split_whitespace(); + while let Some(part) = parts.next() { + let mut file = part.to_string(); + while file.ends_with('\\') { + file.pop(); + file.push(' '); + file.push_str(parts.next().unwrap()); + } + println!("cargo:rerun-if-changed={file}"); + } + } + } + + let generated = format!("pub const GDBSTUB_COMPONENT: &[u8] = include_bytes!({wasm:?});\n"); + std::fs::write(out_dir.join("gen.rs"), generated).unwrap(); +} + +fn cargo() -> Command { + let mut cargo = Command::new("cargo"); + if std::env::var("CARGO_CFG_MIRI").is_ok() { + cargo.env_remove("RUSTC").env_remove("RUSTC_WRAPPER"); + } + cargo +} + +fn rustflags() -> &'static str { + match option_env!("RUSTFLAGS") { + Some(s) if s.contains("-D warnings") => "-D warnings", + _ => "", + } +} diff --git a/crates/gdbstub-component/artifact/src/lib.rs b/crates/gdbstub-component/artifact/src/lib.rs new file mode 100644 index 000000000000..26a930a60ee6 --- /dev/null +++ b/crates/gdbstub-component/artifact/src/lib.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/gen.rs")); diff --git a/crates/gdbstub-component/src/addr.rs b/crates/gdbstub-component/src/addr.rs new file mode 100644 index 000000000000..92d1b0f9754d --- /dev/null +++ b/crates/gdbstub-component/src/addr.rs @@ -0,0 +1,186 @@ +//! Synthetic Wasm address space expected by the gdbstub Wasm +//! extensions. + +use crate::api::{Debuggee, Frame, Memory, Module}; +use anyhow::Result; +use gdbstub_arch::wasm::addr::{WasmAddr, WasmAddrType}; +use std::collections::{HashMap, hash_map::Entry}; + +/// Representation of the synthesized Wasm address space. +pub struct AddrSpace { + module_ids: HashMap, + memory_ids: HashMap, + modules: Vec, + module_bytecode: Vec>, + memories: Vec, +} + +/// The result of a lookup in the address space. +pub enum AddrSpaceLookup<'a> { + Module { + module: &'a Module, + bytecode: &'a [u8], + offset: u32, + }, + Memory { + memory: &'a Memory, + offset: u32, + }, + Empty, +} + +impl AddrSpace { + pub fn new() -> Self { + AddrSpace { + module_ids: HashMap::new(), + modules: vec![], + module_bytecode: vec![], + memory_ids: HashMap::new(), + memories: vec![], + } + } + + fn module_id(&mut self, m: &Module) -> u32 { + match self.module_ids.entry(m.unique_id()) { + Entry::Occupied(o) => *o.get(), + Entry::Vacant(v) => { + let id = u32::try_from(self.modules.len()).unwrap(); + let bytecode = m.bytecode().unwrap_or(vec![]); + self.module_bytecode.push(bytecode); + self.modules.push(m.clone()); + *v.insert(id) + } + } + } + + fn memory_id(&mut self, m: &Memory) -> u32 { + match self.memory_ids.entry(m.unique_id()) { + Entry::Occupied(o) => *o.get(), + Entry::Vacant(v) => { + let id = u32::try_from(self.memories.len()).unwrap(); + self.memories.push(m.clone()); + *v.insert(id) + } + } + } + + /// Update/create new mappings so that all modules and instances' + /// memories in the debuggee have mappings. + pub fn update(&mut self, d: &Debuggee) -> Result<()> { + for module in d.all_modules() { + let _ = self.module_id(&module); + } + for instance in d.all_instances() { + let mut idx = 0; + loop { + if let Ok(m) = instance.get_memory(d, idx) { + let _ = self.memory_id(&m); + idx += 1; + } else { + break; + } + } + } + Ok(()) + } + + /// Iterate over the base `WasmAddr` of every registered module. + pub fn module_base_addrs(&self) -> impl Iterator + '_ { + (0..self.modules.len()) + .map(|idx| WasmAddr::new(WasmAddrType::Object, u32::try_from(idx).unwrap(), 0).unwrap()) + } + + /// Build the GDB memory-map XML describing all known regions. + /// + /// Module bytecode regions are reported as `rom` (read-only), and + /// linear memories as `ram` (read-write). + pub fn memory_map_xml(&self, debuggee: &Debuggee) -> String { + use std::fmt::Write; + let mut xml = String::from( + "", + ); + for (idx, bc) in self.module_bytecode.iter().enumerate() { + let start = + WasmAddr::new(WasmAddrType::Object, u32::try_from(idx).unwrap(), 0).unwrap(); + let len = bc.len(); + if len > 0 { + write!( + xml, + "", + start.as_raw(), + len + ) + .unwrap(); + } + } + for (idx, mem) in self.memories.iter().enumerate() { + let start = + WasmAddr::new(WasmAddrType::Memory, u32::try_from(idx).unwrap(), 0).unwrap(); + let len = mem.size_bytes(debuggee); + if len > 0 { + write!( + xml, + "", + start.as_raw(), + len + ) + .unwrap(); + } + } + xml.push_str(""); + xml + } + + pub fn frame_to_pc(&self, frame: &Frame, debuggee: &Debuggee) -> WasmAddr { + let module = frame.get_instance(debuggee).unwrap().get_module(debuggee); + let &module_id = self + .module_ids + .get(&module.unique_id()) + .expect("module not found in addr space"); + let pc = frame.get_pc(debuggee).unwrap(); + WasmAddr::new(WasmAddrType::Object, module_id, pc).unwrap() + } + + pub fn frame_to_return_addr(&self, frame: &Frame, debuggee: &Debuggee) -> Option { + let module = frame.get_instance(debuggee).unwrap().get_module(debuggee); + let &module_id = self + .module_ids + .get(&module.unique_id()) + .expect("module not found in addr space"); + let ret_pc = frame.get_pc(debuggee).ok()?; + Some(WasmAddr::new(WasmAddrType::Object, module_id, ret_pc).unwrap()) + } + + pub fn lookup(&self, addr: WasmAddr, d: &Debuggee) -> AddrSpaceLookup<'_> { + let index = usize::try_from(addr.module_index()).unwrap(); + match addr.addr_type() { + WasmAddrType::Object => { + if index >= self.modules.len() { + return AddrSpaceLookup::Empty; + } + let bytecode = &self.module_bytecode[index]; + if addr.offset() >= u32::try_from(bytecode.len()).unwrap() { + return AddrSpaceLookup::Empty; + } + AddrSpaceLookup::Module { + module: &self.modules[index], + bytecode, + offset: addr.offset(), + } + } + WasmAddrType::Memory => { + if index >= self.memories.len() { + return AddrSpaceLookup::Empty; + } + let size = self.memories[index].size_bytes(d); + if u64::from(addr.offset()) >= size { + return AddrSpaceLookup::Empty; + } + AddrSpaceLookup::Memory { + memory: &self.memories[index], + offset: addr.offset(), + } + } + } + } +} diff --git a/crates/gdbstub-component/src/api.rs b/crates/gdbstub-component/src/api.rs new file mode 100644 index 000000000000..a33e3057a923 --- /dev/null +++ b/crates/gdbstub-component/src/api.rs @@ -0,0 +1,44 @@ +//! Bindings for Wasmtime's debugger API. + +use wstd::runtime::AsyncPollable; + +wit_bindgen::generate!({ + world: "bytecodealliance:wasmtime/debug-main", + path: "../debugger/wit", + with: { + "wasi:io/poll@0.2.6": wasip2::io::poll, + } +}); +pub(crate) use bytecodealliance::wasmtime::debuggee::*; + +/// One "resumption", or period of execution, in the debuggee. +pub struct Resumption { + future: EventFuture, + pollable: Option, +} + +impl Resumption { + pub fn continue_(d: &Debuggee, r: ResumptionValue) -> Self { + let future = d.continue_(r); + let pollable = Some(AsyncPollable::new(future.subscribe())); + Resumption { future, pollable } + } + + pub fn single_step(d: &Debuggee, r: ResumptionValue) -> Self { + let future = d.single_step(r); + let pollable = Some(AsyncPollable::new(future.subscribe())); + Resumption { future, pollable } + } + + pub async fn wait(&mut self) { + if let Some(pollable) = self.pollable.as_mut() { + pollable.wait_for().await; + } + } + + pub fn result(mut self, d: &Debuggee) -> std::result::Result { + // Drop the pollable first, since it's a child resource. + let _ = self.pollable.take(); + EventFuture::finish(self.future, d) + } +} diff --git a/crates/gdbstub-component/src/lib.rs b/crates/gdbstub-component/src/lib.rs new file mode 100644 index 000000000000..9fea0218eb6e --- /dev/null +++ b/crates/gdbstub-component/src/lib.rs @@ -0,0 +1,351 @@ +//! gdbstub protocol implementation in Wasmtime's debug-main world. + +mod addr; +mod api; +mod target; + +use crate::{ + addr::AddrSpace, + api::{WasmType, WasmValue}, +}; +use anyhow::Result; +use futures::{FutureExt, select}; +use gdbstub::{ + common::{Signal, Tid}, + conn::Connection, + stub::{ + MultiThreadStopReason, + state_machine::{GdbStubStateMachine, GdbStubStateMachineInner, state::Running}, + }, +}; +use gdbstub_arch::wasm::addr::WasmAddr; +use log::trace; +use structopt::StructOpt; +use wstd::{ + io::{AsyncRead, AsyncWrite}, + iter::AsyncIterator, + net::{TcpListener, TcpStream}, +}; + +/// Command-line options. +#[derive(StructOpt)] +struct Options { + /// The TCP address to listen on, in `:` format. + tcp_address: String, + /// Verbose logging. + #[structopt(short = "v")] + verbose: bool, +} + +struct Component; +api::export!(Component with_types_in api); + +impl api::exports::bytecodealliance::wasmtime::debugger::Guest for Component { + fn debug(d: &api::Debuggee, args: Vec) { + let options = Options::from_iter(args); + if options.verbose { + env_logger::Builder::new() + .filter_level(log::LevelFilter::Trace) + .init(); + } + let mut debugger = Debugger { + debuggee: d, + tid: Tid::new(1).unwrap(), + options, + running: None, + current_pc: WasmAddr::from_raw(0).unwrap(), + interrupt: false, + single_stepping: false, + frame_cache: vec![], + addr_space: AddrSpace::new(), + }; + wstd::runtime::block_on(async { + if let Err(e) = debugger.run().await { + trace!("debugger exited with error: {e}"); + } + }); + } +} + +struct Debugger<'a> { + debuggee: &'a api::Debuggee, + tid: Tid, + options: Options, + running: Option, + addr_space: AddrSpace, + interrupt: bool, + single_stepping: bool, + current_pc: WasmAddr, + frame_cache: Vec, +} + +impl<'a> Debugger<'a> { + async fn run(&mut self) -> Result<()> { + // Single-step once so modules are loaded and PC is at the + // first instruction. + self.start_single_step(api::ResumptionValue::Normal); + self.running.as_mut().unwrap().wait().await; + let _ = self.running.take().unwrap().result(self.debuggee)?; + self.update_on_stop(); + + let listener = TcpListener::bind(&self.options.tcp_address) + .await + .expect("Could not bind to TCP port"); + + // Only accept one connection for the run; once the debugger + // disconnects, we'll just continue. + let Some(connection) = listener.incoming().next().await else { + return Ok(()); + }; + + let gdbconn = Conn::new(connection?); + let mut stub = gdbstub::stub::GdbStub::new(gdbconn).run_state_machine(&mut *self)?; + + // Main loop. + 'mainloop: loop { + match stub { + GdbStubStateMachine::Idle(mut inner) => { + if inner.borrow_conn().flush().await.is_err() { + // Connection closed or other outbound error. + break 'mainloop; + } + + // Wait for an inbound network byte. + let Some(byte) = inner.borrow_conn().read_byte().await? else { + inner.borrow_conn().flush().await?; + break 'mainloop; + }; + + stub = inner.incoming_data(self, byte)?; + } + + GdbStubStateMachine::Running(mut inner) => { + if inner.borrow_conn().flush().await.is_err() { + // Connection closed or other outbound error. + break 'mainloop; + } + + // Wait for either a resumption or a byte from the + // connection. + let resumption = self + .running + .as_mut() + .expect("In Running state, we must have a resumption future"); + select! { + _ = resumption.wait().fuse() => { + let resumption = self.running.take().unwrap(); + let event = resumption.result(self.debuggee)?; + stub = self.handle_event(event, inner).await?; + } + byte = inner.borrow_conn().read_byte().fuse() => { + let Some(byte) = byte? else { + // Eat any connection-closed errors on + // the outbound flush. + let _ = inner.borrow_conn().flush().await; + // Connection closed. + break 'mainloop; + }; + stub = inner.incoming_data(&mut *self, byte)?; + } + } + } + GdbStubStateMachine::CtrlCInterrupt(mut inner) => { + if inner.borrow_conn().flush().await.is_err() { + // Connection error: break. + break 'mainloop; + } + stub = inner.interrupt_handled(self, None::>)?; + } + GdbStubStateMachine::Disconnected(mut inner) => { + // Eat any connection-closed errors -- we are + // already in Disconnected state. + let _ = inner.borrow_conn().flush().await; + break 'mainloop; + } + } + } + + Ok(()) + } + + fn start_continue(&mut self, resumption: api::ResumptionValue) { + assert!(self.running.is_none()); + trace!("continuing"); + self.single_stepping = false; + self.running = Some(api::Resumption::continue_(self.debuggee, resumption)); + } + + fn start_single_step(&mut self, resumption: api::ResumptionValue) { + assert!(self.running.is_none()); + trace!("single-stepping"); + self.single_stepping = true; + self.running = Some(api::Resumption::single_step(self.debuggee, resumption)); + } + + fn update_on_stop(&mut self) { + self.addr_space.update(self.debuggee).unwrap(); + + // Cache all frame handles for the duration of this stop. + // The Wasm trait methods take `&self` and need access to + // frames by depth, so we eagerly walk the full stack here. + self.frame_cache.clear(); + let mut next = self.debuggee.exit_frames().into_iter().next(); + while let Some(f) = next { + next = f.parent_frame(self.debuggee).unwrap(); + self.frame_cache.push(f); + } + + if let Some(f) = self.frame_cache.first() { + self.current_pc = self.addr_space.frame_to_pc(f, self.debuggee); + } else { + self.current_pc = WasmAddr::from_raw(0).unwrap(); + } + } + + async fn handle_event<'b>( + &mut self, + event: api::Event, + inner: GdbStubStateMachineInner<'b, Running, Self, Conn>, + ) -> Result> { + match event { + api::Event::Complete => { + trace!("Event::Complete"); + let pc_bytes = self.current_pc.as_raw().to_le_bytes(); + let mut regs = core::iter::once(( + gdbstub_arch::wasm::reg::id::WasmRegId::Pc, + pc_bytes.as_slice(), + )); + Ok(inner.report_stop_with_regs( + self, + MultiThreadStopReason::Exited(0), + &mut regs, + )?) + } + api::Event::Breakpoint => { + trace!( + "Event::Breakpoint; single_stepping = {}", + self.single_stepping + ); + self.update_on_stop(); + let stop_reason = if self.single_stepping { + MultiThreadStopReason::SignalWithThread { + tid: self.tid, + signal: Signal::SIGTRAP, + } + } else { + MultiThreadStopReason::SwBreak(self.tid) + }; + let pc_bytes = self.current_pc.as_raw().to_le_bytes(); + let mut regs = core::iter::once(( + gdbstub_arch::wasm::reg::id::WasmRegId::Pc, + pc_bytes.as_slice(), + )); + Ok(inner.report_stop_with_regs(self, stop_reason, &mut regs)?) + } + api::Event::Trap => { + trace!("Event::Trap"); + self.update_on_stop(); + let pc_bytes = self.current_pc.as_raw().to_le_bytes(); + let mut regs = core::iter::once(( + gdbstub_arch::wasm::reg::id::WasmRegId::Pc, + pc_bytes.as_slice(), + )); + Ok(inner.report_stop_with_regs( + self, + // We report all traps as SIGSEGV for now. + MultiThreadStopReason::SignalWithThread { + tid: self.tid, + signal: Signal::SIGSEGV, + }, + &mut regs, + )?) + } + _ => { + trace!("other event: {event:?}"); + if self.interrupt { + self.interrupt = false; + self.update_on_stop(); + let pc_bytes = self.current_pc.as_raw().to_le_bytes(); + let mut regs = core::iter::once(( + gdbstub_arch::wasm::reg::id::WasmRegId::Pc, + pc_bytes.as_slice(), + )); + Ok(inner.report_stop_with_regs( + self, + MultiThreadStopReason::Signal(Signal::SIGINT), + &mut regs, + )?) + } else { + if self.single_stepping { + self.start_single_step(api::ResumptionValue::Normal); + } else { + self.start_continue(api::ResumptionValue::Normal); + } + Ok(GdbStubStateMachine::Running(inner)) + } + } + } + } + + fn value_to_bytes(&self, value: WasmValue) -> Vec { + match value.get_type() { + WasmType::WasmI32 => value.unwrap_i32().to_le_bytes().to_vec(), + WasmType::WasmI64 => value.unwrap_i64().to_le_bytes().to_vec(), + WasmType::WasmF32 => value.unwrap_f32().to_le_bytes().to_vec(), + WasmType::WasmF64 => value.unwrap_f64().to_le_bytes().to_vec(), + WasmType::WasmV128 => value.unwrap_v128(), + WasmType::WasmFuncref => 0u32.to_le_bytes().to_vec(), + WasmType::WasmExnref => 0u32.to_le_bytes().to_vec(), + } + } +} + +struct Conn { + buf: Vec, + conn: TcpStream, +} + +impl Conn { + fn new(conn: TcpStream) -> Self { + Conn { buf: vec![], conn } + } + + async fn flush(&mut self) -> anyhow::Result<()> { + self.conn.write_all(&self.buf).await?; + self.buf.clear(); + Ok(()) + } + + async fn read_byte(&mut self) -> Result> { + let mut buf = [0u8]; + let len = self.conn.read(&mut buf).await?; + if len == 1 { Ok(Some(buf[0])) } else { Ok(None) } + } +} + +impl Drop for Conn { + fn drop(&mut self) { + assert!( + self.buf.is_empty(), + "failed to async-flush before dropping connection write buffer" + ); + } +} + +impl Connection for Conn { + type Error = anyhow::Error; + + fn write(&mut self, byte: u8) -> std::result::Result<(), Self::Error> { + self.buf.push(byte); + Ok(()) + } + + fn flush(&mut self) -> std::result::Result<(), Self::Error> { + // We cannot flush synchronously; we leave this to the `async + // fn flush` method called within the main loop. Fortunately + // the gdbstub cannot wait for a response before returning to + // the main loop, so we cannot introduce any deadlocks by + // failing to flush synchronously here. + Ok(()) + } +} diff --git a/crates/gdbstub-component/src/target.rs b/crates/gdbstub-component/src/target.rs new file mode 100644 index 000000000000..e76165517070 --- /dev/null +++ b/crates/gdbstub-component/src/target.rs @@ -0,0 +1,462 @@ +//! gdbstub `Target` implementation. + +use crate::Debugger; +use crate::addr::AddrSpaceLookup; +use crate::api; +use gdbstub::arch::lldb::{Encoding, Format, Generic, Register}; +use gdbstub::common::{Endianness, Pid, Signal, Tid}; +use gdbstub::target::Target; +use gdbstub::target::TargetError; +use gdbstub::target::TargetResult; +use gdbstub::target::ext::base::BaseOps; +use gdbstub::target::ext::base::multithread::{ + MultiThreadBase, MultiThreadResume, MultiThreadResumeOps, MultiThreadSchedulerLocking, + MultiThreadSchedulerLockingOps, MultiThreadSingleStep, MultiThreadSingleStepOps, +}; +use gdbstub::target::ext::base::single_register_access::{ + SingleRegisterAccess, SingleRegisterAccessOps, +}; +use gdbstub::target::ext::breakpoints::{ + Breakpoints, BreakpointsOps, SwBreakpoint, SwBreakpointOps, +}; +use gdbstub::target::ext::host_info::{HostInfo, HostInfoOps, HostInfoResponse}; +use gdbstub::target::ext::libraries::{Libraries, LibrariesOps}; +use gdbstub::target::ext::lldb_register_info_override::{ + Callback, CallbackToken, LldbRegisterInfoOverride, LldbRegisterInfoOverrideOps, +}; +use gdbstub::target::ext::memory_map::{MemoryMap, MemoryMapOps}; +use gdbstub::target::ext::process_info::{ProcessInfo, ProcessInfoOps, ProcessInfoResponse}; +use gdbstub::target::ext::wasm::{Wasm, WasmOps}; +use gdbstub_arch::wasm::Wasm as WasmArch; +use gdbstub_arch::wasm::addr::WasmAddr; +use gdbstub_arch::wasm::reg::WasmRegisters; +use gdbstub_arch::wasm::reg::id::WasmRegId; + +impl<'a> Target for Debugger<'a> { + type Arch = WasmArch; + type Error = anyhow::Error; + + fn base_ops(&mut self) -> BaseOps<'_, Self::Arch, Self::Error> { + BaseOps::MultiThread(self) + } + + fn support_wasm(&mut self) -> Option> { + Some(self) + } + + fn use_lldb_register_info(&self) -> bool { + true + } + + fn support_lldb_register_info_override( + &mut self, + ) -> Option> { + Some(self) + } + + fn support_breakpoints(&mut self) -> Option> { + Some(self) + } + + fn support_libraries(&mut self) -> Option> { + Some(self) + } + + fn support_memory_map(&mut self) -> Option> { + Some(self) + } + + fn support_process_info(&mut self) -> Option> { + Some(self) + } + + fn support_host_info(&mut self) -> Option> { + Some(self) + } +} + +impl<'a> MultiThreadBase for Debugger<'a> { + fn read_registers(&mut self, regs: &mut WasmRegisters, _tid: Tid) -> TargetResult<(), Self> { + regs.pc = self.current_pc.as_raw(); + Ok(()) + } + + fn write_registers(&mut self, regs: &WasmRegisters, _tid: Tid) -> TargetResult<(), Self> { + self.current_pc = WasmAddr::from_raw(regs.pc).ok_or(TargetError::NonFatal)?; + Ok(()) + } + + fn read_addrs( + &mut self, + start_addr: u64, + data: &mut [u8], + _tid: Tid, + ) -> TargetResult { + let addr = WasmAddr::from_raw(start_addr).ok_or(TargetError::NonFatal)?; + let debuggee = self.debuggee; + match self.addr_space.lookup(addr, debuggee) { + AddrSpaceLookup::Module { + bytecode, offset, .. + } => { + let offset = usize::try_from(offset).unwrap(); + let avail = bytecode.len() - offset; + let n = avail.min(data.len()); + data[..n].copy_from_slice(&bytecode[offset..offset + n]); + Ok(n) + } + AddrSpaceLookup::Memory { memory, offset } => { + match memory.get_bytes(debuggee, offset.into(), u64::try_from(data.len()).unwrap()) + { + Ok(bytes) => { + assert_eq!(bytes.len(), data.len()); + data.copy_from_slice(&bytes); + Ok(data.len()) + } + Err(_) => Err(TargetError::NonFatal), + } + } + AddrSpaceLookup::Empty => Err(TargetError::NonFatal), + } + } + + fn write_addrs(&mut self, _start_addr: u64, _data: &[u8], _tid: Tid) -> TargetResult<(), Self> { + Err(TargetError::NonFatal) + } + + #[inline(always)] + fn list_active_threads( + &mut self, + thread_is_active: &mut dyn FnMut(Tid), + ) -> Result<(), Self::Error> { + thread_is_active(self.tid); + Ok(()) + } + + fn support_single_register_access(&mut self) -> Option> { + Some(self) + } + + fn support_resume(&mut self) -> Option> { + Some(self) + } +} + +impl<'a> SingleRegisterAccess for Debugger<'a> { + fn read_register( + &mut self, + _tid: Tid, + reg_id: WasmRegId, + buf: &mut [u8], + ) -> TargetResult { + match reg_id { + WasmRegId::Pc => { + let bytes = self.current_pc.as_raw().to_le_bytes(); + let n = bytes.len().min(buf.len()); + buf[..n].copy_from_slice(&bytes[..n]); + Ok(n) + } + _ => Err(TargetError::NonFatal), + } + } + + fn write_register( + &mut self, + _tid: Tid, + reg_id: WasmRegId, + val: &[u8], + ) -> TargetResult<(), Self> { + match reg_id { + WasmRegId::Pc => { + if val.len() < 8 { + return Err(TargetError::NonFatal); + } + let raw = u64::from_le_bytes(val[..8].try_into().unwrap()); + self.current_pc = WasmAddr::from_raw(raw).ok_or(TargetError::NonFatal)?; + Ok(()) + } + _ => Err(TargetError::NonFatal), + } + } +} + +impl<'a> MultiThreadResume for Debugger<'a> { + fn resume(&mut self) -> Result<(), Self::Error> { + self.frame_cache.clear(); + log::trace!("resume() -> single_stepping = {}", self.single_stepping); + if self.single_stepping { + self.start_single_step(api::ResumptionValue::Normal); + } else { + self.start_continue(api::ResumptionValue::Normal); + } + Ok(()) + } + + fn clear_resume_actions(&mut self) -> Result<(), Self::Error> { + self.single_stepping = false; + Ok(()) + } + + fn set_resume_action_continue( + &mut self, + _tid: Tid, + _signal: Option, + ) -> Result<(), Self::Error> { + self.single_stepping = false; + Ok(()) + } + + fn support_single_step(&mut self) -> Option> { + Some(self) + } + + fn support_scheduler_locking(&mut self) -> Option> { + Some(self) + } +} + +impl<'a> MultiThreadSingleStep for Debugger<'a> { + fn set_resume_action_step( + &mut self, + _tid: Tid, + _signal: Option, + ) -> Result<(), Self::Error> { + self.single_stepping = true; + Ok(()) + } +} + +impl<'a> MultiThreadSchedulerLocking for Debugger<'a> { + fn set_resume_action_scheduler_lock(&mut self) -> Result<(), Self::Error> { + // We have a single thread, so scheduler locking is a no-op. + Ok(()) + } +} + +impl<'a> Breakpoints for Debugger<'a> { + #[inline(always)] + fn support_sw_breakpoint(&mut self) -> Option> { + Some(self) + } +} + +impl<'a> SwBreakpoint for Debugger<'a> { + fn add_sw_breakpoint(&mut self, addr: u64, _kind: usize) -> TargetResult { + let Some(wasm_addr) = WasmAddr::from_raw(addr) else { + return Ok(false); + }; + let debuggee = self.debuggee; + if let AddrSpaceLookup::Module { module, .. } = self.addr_space.lookup(wasm_addr, debuggee) + { + module + .add_breakpoint(debuggee, wasm_addr.offset()) + .map_err(|_| TargetError::NonFatal)?; + Ok(true) + } else { + Ok(false) + } + } + + fn remove_sw_breakpoint(&mut self, addr: u64, _kind: usize) -> TargetResult { + let Some(wasm_addr) = WasmAddr::from_raw(addr) else { + return Ok(false); + }; + let debuggee = self.debuggee; + if let AddrSpaceLookup::Module { module, .. } = self.addr_space.lookup(wasm_addr, debuggee) + { + module + .remove_breakpoint(debuggee, wasm_addr.offset()) + .map_err(|_| TargetError::NonFatal)?; + Ok(true) + } else { + Ok(false) + } + } +} + +impl<'a> LldbRegisterInfoOverride for Debugger<'a> { + fn lldb_register_info<'b>( + &mut self, + reg_id: usize, + reg_info: Callback<'b>, + ) -> Result, Self::Error> { + Ok(match reg_id { + 0 => reg_info.write(Register { + name: "pc", + alt_name: Some("pc"), + bitsize: 64, + offset: 0, + encoding: Encoding::Uint, + format: Format::Hex, + set: "PC", + gcc: Some(16), + dwarf: Some(16), + generic: Some(Generic::Pc), + container_regs: None, + invalidate_regs: None, + }), + _ => reg_info.done(), + }) + } +} + +impl<'a> Libraries for Debugger<'a> { + fn get_libraries( + &self, + offset: u64, + length: usize, + buf: &mut [u8], + ) -> TargetResult { + let mut xml = String::from(""); + for addr in self.addr_space.module_base_addrs() { + xml.push_str(&format!( + "
", + addr.as_raw() + )); + } + xml.push_str(""); + + let xml_bytes = xml.as_bytes(); + let offset = usize::try_from(offset).unwrap(); + if offset >= xml_bytes.len() { + return Ok(0); + } + let avail = xml_bytes.len() - offset; + let n = avail.min(length).min(buf.len()); + buf[..n].copy_from_slice(&xml_bytes[offset..offset + n]); + Ok(n) + } +} + +impl<'a> MemoryMap for Debugger<'a> { + fn memory_map_xml( + &self, + offset: u64, + length: usize, + buf: &mut [u8], + ) -> TargetResult { + let xml = self.addr_space.memory_map_xml(self.debuggee); + let xml_bytes = xml.as_bytes(); + let offset = usize::try_from(offset).unwrap(); + if offset >= xml_bytes.len() { + return Ok(0); + } + let avail = xml_bytes.len() - offset; + let n = avail.min(length).min(buf.len()); + buf[..n].copy_from_slice(&xml_bytes[offset..offset + n]); + Ok(n) + } +} + +impl<'a> Wasm for Debugger<'a> { + fn wasm_call_stack(&self, _tid: Tid, callback: &mut dyn FnMut(u64)) -> Result<(), Self::Error> { + let debuggee = self.debuggee; + for (i, f) in self.frame_cache.iter().enumerate() { + // For non-innermost frames, report the return address + // (the instruction after the call) rather than the call + // instruction's PC. This matches the standard debugger + // convention and is needed for LLDB's `finish` command + // to set a breakpoint at the right address. + let pc = if i > 0 { + self.addr_space + .frame_to_return_addr(f, debuggee) + .unwrap_or_else(|| self.addr_space.frame_to_pc(f, debuggee)) + } else { + self.addr_space.frame_to_pc(f, debuggee) + }; + callback(pc.as_raw()); + } + Ok(()) + } + + fn read_wasm_local( + &self, + _tid: Tid, + frame_depth: usize, + index: usize, + buf: &mut [u8], + ) -> Result { + let Some(f) = self.frame_cache.get(frame_depth) else { + return Ok(0); + }; + let Ok(locals) = f.get_locals(self.debuggee) else { + return Ok(0); + }; + let Some(val) = locals.get(index) else { + return Ok(0); + }; + let bytes = self.value_to_bytes(val.clone()); + buf[..bytes.len()].copy_from_slice(&bytes); + Ok(bytes.len()) + } + + fn read_wasm_global( + &self, + _tid: Tid, + frame_depth: usize, + index: usize, + buf: &mut [u8], + ) -> Result { + let Some(f) = self.frame_cache.get(frame_depth) else { + return Ok(0); + }; + let debuggee = self.debuggee; + let Ok(instance) = f.get_instance(debuggee) else { + return Ok(0); + }; + let Ok(global) = instance.get_global(debuggee, u32::try_from(index).unwrap()) else { + return Ok(0); + }; + let Ok(val) = global.get(debuggee) else { + return Ok(0); + }; + let bytes = self.value_to_bytes(val); + buf[..bytes.len()].copy_from_slice(&bytes); + Ok(bytes.len()) + } + + fn read_wasm_stack( + &self, + _tid: Tid, + frame_depth: usize, + index: usize, + buf: &mut [u8], + ) -> Result { + let Some(f) = self.frame_cache.get(frame_depth) else { + return Ok(0); + }; + let Ok(stack) = f.get_stack(self.debuggee) else { + return Ok(0); + }; + let Some(val) = stack.get(index) else { + return Ok(0); + }; + let bytes = self.value_to_bytes(val.clone()); + buf[..bytes.len()].copy_from_slice(&bytes); + Ok(bytes.len()) + } +} + +impl<'a> HostInfo for Debugger<'a> { + fn host_info( + &self, + write_item: &mut dyn FnMut(&HostInfoResponse<'_>), + ) -> Result<(), Self::Error> { + write_item(&HostInfoResponse::Triple("wasm32-unknown-unknown-wasm")); + write_item(&HostInfoResponse::Endianness(Endianness::Little)); + write_item(&HostInfoResponse::PointerSize(4)); + Ok(()) + } +} + +impl<'a> ProcessInfo for Debugger<'a> { + fn process_info( + &self, + write_item: &mut dyn FnMut(&ProcessInfoResponse<'_>), + ) -> Result<(), Self::Error> { + write_item(&ProcessInfoResponse::Pid(Pid::new(1).unwrap())); + write_item(&ProcessInfoResponse::Triple("wasm32-unknown-unknown-wasm")); + write_item(&ProcessInfoResponse::Endianness(Endianness::Little)); + write_item(&ProcessInfoResponse::PointerSize(4)); + Ok(()) + } +} diff --git a/src/commands/run.rs b/src/commands/run.rs index 6ed6c6ae2ee8..12797270809f 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -65,6 +65,14 @@ pub struct RunCommand { #[arg(long)] pub argv0: Option, + /// Override the module bytes loaded from disk. When set, the + /// first positional argument is ignored for loading purposes and + /// these bytes are used instead. This is not a CLI option; it is + /// used internally to inject pre-built bytes (e.g. for an + /// included debug adapter). + #[arg(skip)] + pub module_bytes: Option>, + /// The WebAssembly module to run and arguments to pass to it. /// /// Arguments passed to the wasm module will be configured as WASI CLI @@ -101,6 +109,19 @@ impl RunCommand { Ok(()) } + // When -g is specified, set up the debugger path and args from + // the built-in gdbstub component. + let override_bytes = if let Some(port) = self.run.gdbstub_port { + if self.run.common.debug.debugger.is_some() { + bail!("-g/--gdb cannot be combined with -Ddebugger="); + } + self.run.common.debug.debugger = Some("".into()); + self.run.common.debug.arg.push(format!("0.0.0.0:{port}")); + Some(gdbstub_component_artifact::GDBSTUB_COMPONENT.to_vec()) + } else { + None + }; + if let Some(debugger_component_path) = self.run.common.debug.debugger.as_ref() { set_implicit_option( "debuggee", @@ -120,6 +141,7 @@ impl RunCommand { .into_iter() .chain(self.run.common.debug.arg.iter().map(OsString::from)), )?; + debugger_run.module_bytes = override_bytes; // Explicitly permit TCP sockets for the debugger-main // environment, if not already set. @@ -208,17 +230,21 @@ impl RunCommand { let debug_run = self.debugger_run()?; let engine = self.new_engine()?; - let main = self - .run - .load_module(&engine, self.module_and_args[0].as_ref())?; + let main = self.run.load_module( + &engine, + self.module_and_args[0].as_ref(), + self.module_bytes.as_ref().map(|v| &v[..]), + )?; let (mut store, mut linker) = self.new_store_and_linker(&engine, &main)?; #[cfg(feature = "debug")] if let Some(mut debug_run) = debug_run { let debug_engine = debug_run.new_engine()?; - let debug_main = debug_run - .run - .load_module(&debug_engine, debug_run.module_and_args[0].as_ref())?; + let debug_main = debug_run.run.load_module( + &debug_engine, + debug_run.module_and_args[0].as_ref(), + debug_run.module_bytes.as_ref().map(|v| &v[..]), + )?; let (mut debug_store, debug_linker) = debug_run.new_store_and_linker(&debug_engine, &debug_main)?; @@ -378,7 +404,7 @@ impl RunCommand { // Load the preload wasm modules. for (name, path) in self.preloads.modules.iter() { // Read the wasm module binary either as `*.wat` or a raw binary - let preload_target = self.run.load_module(&engine, path)?; + let preload_target = self.run.load_module(&engine, path, None)?; let preload_module = match preload_target { RunTarget::Core(m) => m, #[cfg(feature = "component-model")] diff --git a/src/commands/serve.rs b/src/commands/serve.rs index 4badbe845734..390bf270138e 100644 --- a/src/commands/serve.rs +++ b/src/commands/serve.rs @@ -414,7 +414,7 @@ impl ServeCommand { self.add_to_linker(&mut linker)?; - let component = match self.run.load_module(&engine, &self.component)? { + let component = match self.run.load_module(&engine, &self.component, None)? { RunTarget::Core(_) => bail!("The serve command currently requires a component"), RunTarget::Component(c) => c, }; diff --git a/src/commands/wizer.rs b/src/commands/wizer.rs index 4fa36eae5489..e221904cd665 100644 --- a/src/commands/wizer.rs +++ b/src/commands/wizer.rs @@ -84,6 +84,7 @@ impl WizerCommand { }), module_and_args: vec![self.input.clone().into()], preloads: self.preloads.clone(), + module_bytes: None, }; let engine = run.new_engine()?; diff --git a/src/common.rs b/src/common.rs index 982417fffb3d..7d2e9ddc965b 100644 --- a/src/common.rs +++ b/src/common.rs @@ -100,6 +100,13 @@ pub struct RunCommon { /// cause the environment variable `FOO` to be inherited. #[arg(long = "env", number_of_values = 1, value_name = "NAME[=VAL]", value_parser = parse_env_var)] pub vars: Vec<(String, Option)>, + + /// Attach the built-in gdbstub debugger component, listening on the + /// given TCP port. A debugger (e.g. LLDB) can then connect via + /// `process connect --plugin=wasm connect://localhost:`. + #[cfg(feature = "debug")] + #[arg(short = 'g', long = "gdbstub", value_name = "PORT")] + pub gdbstub_port: Option, } fn parse_env_var(s: &str) -> Result<(String, Option)> { @@ -162,53 +169,69 @@ impl RunCommon { Ok(()) } - pub fn load_module(&self, engine: &Engine, path: &Path) -> Result { + pub fn load_module( + &self, + engine: &Engine, + path: &Path, + preloaded_bytes: Option<&[u8]>, + ) -> Result { let path = match path.to_str() { #[cfg(unix)] Some("-") => "/dev/stdin".as_ref(), _ => path, }; - let file = - File::open(path).with_context(|| format!("failed to open wasm module {path:?}"))?; - - // First attempt to load the module as an mmap. If this succeeds then - // detection can be done with the contents of the mmap and if a - // precompiled module is detected then `deserialize_file` can be used - // which is a slightly more optimal version than `deserialize` since we - // can leave most of the bytes on disk until they're referenced. - // - // If the mmap fails, for example if stdin is a pipe, then fall back to - // `std::fs::read` to load the contents. At that point precompiled - // modules must go through the `deserialize` functions. - // - // Note that this has the unfortunate side effect for precompiled - // modules on disk that they're opened once to detect what they are and - // then again internally in Wasmtime as part of the `deserialize_file` - // API. Currently there's no way to pass the `MmapVec` here through to - // Wasmtime itself (that'd require making `MmapVec` a public type, both - // which isn't ready to happen at this time). It's hoped though that - // opening a file twice isn't too bad in the grand scheme of things with - // respect to the CLI. - match wasmtime::_internal::MmapVec::from_file(file) { - Ok(map) => self.load_module_contents( + if let Some(bytes) = preloaded_bytes { + self.load_module_contents( engine, path, - &map, - || unsafe { Module::deserialize_file(engine, path) }, + &bytes, + || unsafe { Module::deserialize(engine, &bytes) }, #[cfg(feature = "component-model")] - || unsafe { Component::deserialize_file(engine, path) }, - ), - Err(_) => { - let bytes = std::fs::read(path) - .with_context(|| format!("failed to read file: {}", path.display()))?; - self.load_module_contents( + || unsafe { Component::deserialize(engine, &bytes) }, + ) + } else { + let file = + File::open(path).with_context(|| format!("failed to open wasm module {path:?}"))?; + + // First attempt to load the module as an mmap. If this succeeds then + // detection can be done with the contents of the mmap and if a + // precompiled module is detected then `deserialize_file` can be used + // which is a slightly more optimal version than `deserialize` since we + // can leave most of the bytes on disk until they're referenced. + // + // If the mmap fails, for example if stdin is a pipe, then fall back to + // `std::fs::read` to load the contents. At that point precompiled + // modules must go through the `deserialize` functions. + // + // Note that this has the unfortunate side effect for precompiled + // modules on disk that they're opened once to detect what they are and + // then again internally in Wasmtime as part of the `deserialize_file` + // API. Currently there's no way to pass the `MmapVec` here through to + // Wasmtime itself (that'd require making `MmapVec` a public type, both + // which isn't ready to happen at this time). It's hoped though that + // opening a file twice isn't too bad in the grand scheme of things with + // respect to the CLI. + match wasmtime::_internal::MmapVec::from_file(file) { + Ok(map) => self.load_module_contents( engine, path, - &bytes, - || unsafe { Module::deserialize(engine, &bytes) }, + &map, + || unsafe { Module::deserialize_file(engine, path) }, #[cfg(feature = "component-model")] - || unsafe { Component::deserialize(engine, &bytes) }, - ) + || unsafe { Component::deserialize_file(engine, path) }, + ), + Err(_) => { + let bytes = std::fs::read(path) + .with_context(|| format!("failed to read file: {}", path.display()))?; + self.load_module_contents( + engine, + path, + &bytes, + || unsafe { Module::deserialize(engine, &bytes) }, + #[cfg(feature = "component-model")] + || unsafe { Component::deserialize(engine, &bytes) }, + ) + } } } } diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index 0801793991ab..220f601de288 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -2051,6 +2051,12 @@ criteria = "safe-to-deploy" version = "0.1.6" notes = "Contains no unsafe code, no IO, no build.rs." +[[audits.ansi_term]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +version = "0.12.1" +notes = "Only unsafe code is to access the console on Windows." + [[audits.anyhow]] who = "Alex Crichton " criteria = "safe-to-deploy" @@ -2660,6 +2666,11 @@ criteria = "safe-to-deploy" version = "0.4.4" notes = "Most unsafe is hidden by `inout` dependency; only remaining unsafe is raw-splitting a slice and an unreachable hint. Older versions of this regularly reach ~150k daily downloads." +[[audits.clap]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +version = "2.34.0" + [[audits.cobs]] who = "Alex Crichton " criteria = "safe-to-deploy" @@ -3159,6 +3170,16 @@ who = "Pat Hickey " criteria = "safe-to-deploy" delta = "0.6.0 -> 0.8.1" +[[audits.gdbstub]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +version = "0.7.10" + +[[audits.gdbstub_arch]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +version = "0.3.3" + [[audits.gimli]] who = "Alex Crichton " criteria = "safe-to-deploy" @@ -3253,12 +3274,22 @@ criteria = "safe-to-deploy" version = "0.4.0" notes = "Contains `forbid_unsafe` and only uses `std::fmt` from the standard library. Otherwise only contains string manipulation." +[[audits.heck]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +delta = "0.4.0 -> 0.3.3" + [[audits.heck]] who = "Alex Crichton " criteria = "safe-to-deploy" delta = "0.4.1 -> 0.5.0" notes = "Minor changes for a `no_std` upgrade but otherwise everything looks as expected." +[[audits.hermit-abi]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +version = "0.1.19" + [[audits.hermit-abi]] who = "Pat Hickey " criteria = "safe-to-deploy" @@ -3744,6 +3775,12 @@ criteria = "safe-to-deploy" delta = "0.4.1 -> 0.4.2" notes = "It does unsafe FFI bindings, as expected. I didn't check the FFI bindings against the C headers." +[[audits.managed]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +version = "0.8.0" +notes = "No unsafe code." + [[audits.matchers]] who = "Pat Hickey " criteria = "safe-to-deploy" @@ -4143,6 +4180,12 @@ criteria = "safe-to-deploy" version = "2.2.1" notes = "forbid-unsafe crate with straightforward imports." +[[audits.pastey]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +version = "0.2.1" +notes = "No unsafe code." + [[audits.peeking_take_while]] who = "Nick Fitzgerald " criteria = "safe-to-deploy" @@ -4217,6 +4260,11 @@ criteria = "safe-to-deploy" delta = "0.4.0 -> 0.5.0" notes = "This is a minor update which bumps the `env_logger` dependency and has other formatting, no major changes." +[[audits.proc-macro-error]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +version = "1.0.4" + [[audits.proc-macro2]] who = "Pat Hickey " criteria = "safe-to-deploy" @@ -4603,6 +4651,21 @@ criteria = "safe-to-deploy" version = "1.1.0" notes = "No dependencies and completely a compile-time crate as advertised. Uses `unsafe` in one module as a compile-time check only: `mem::transmute` and `ptr::write` are wrapped in an impossible-to-run closure." +[[audits.strsim]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +delta = "0.10.0 -> 0.8.0" + +[[audits.structopt]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +version = "0.3.26" + +[[audits.structopt-derive]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +version = "0.4.18" + [[audits.syn]] who = "Pat Hickey " criteria = "safe-to-deploy" @@ -4726,6 +4789,12 @@ criteria = "safe-to-run" delta = "0.2.16 -> 0.2.18" notes = "Standard macro changes, nothing out of place" +[[audits.textwrap]] +who = "Chris Fallin " +criteria = "safe-to-deploy" +version = "0.11.0" +notes = "No unsafe code." + [[audits.thread_local]] who = "Pat Hickey " criteria = "safe-to-deploy" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index 35938b1fda62..c1198cea2856 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -2266,6 +2266,12 @@ criteria = "safe-to-deploy" version = "0.1.0" notes = "No unsafe usage or ambient capabilities, sane build script" +[[audits.embark-studios.audits.vec_map]] +who = "Johan Andersson " +criteria = "safe-to-deploy" +version = "0.8.2" +notes = "No unsafe usage or ambient capabilities" + [[audits.google.audits.addr2line]] who = "George Burgess IV " criteria = "safe-to-run" @@ -2357,6 +2363,12 @@ delta = "0.2.9 -> 0.2.13" notes = "Audited at https://fxrev.dev/946396" aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" +[[audits.google.audits.proc-macro-error-attr]] +who = "George Burgess IV " +criteria = "safe-to-deploy" +version = "1.0.4" +aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" + [[audits.google.audits.rand]] who = "Lukasz Anforowicz " criteria = "safe-to-deploy"