diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index fcfa5d1..ce8c775 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -26,11 +26,14 @@ jobs: run: cargo install wasm-pack@0.13.1 --locked - name: Install worker-build - run: cargo install worker-build@0.7.5 --locked + run: cargo install worker-build@0.8.5 --locked + # RUSTFLAGS: force legacy Wasm EH so wasm-bindgen 0.2.126's wasmparser + # accepts the module (modern exnref opcodes from nightly >=2026-05-09 + # are not yet supported). - name: Build ct_worker (dev environment) working-directory: crates/ct_worker - run: DEPLOY_ENV=dev worker-build --release + run: DEPLOY_ENV=dev RUSTFLAGS='-Cllvm-args=-wasm-use-legacy-eh' worker-build --release --panic-unwind # .dev.vars contains the per-shard signing and witness keys used by # wrangler dev. These are dev-only keys (not production secrets) and are @@ -74,12 +77,13 @@ jobs: run: cargo install wasm-pack@0.13.1 --locked - name: Install worker-build - run: cargo install worker-build@0.7.5 --locked + run: cargo install worker-build@0.8.5 --locked + # RUSTFLAGS: force legacy Wasm EH (see ct_worker build comment). - name: Build bootstrap_mtc_worker (dev environment, with dev-bootstrap-roots) working-directory: crates/bootstrap_mtc_worker # wrangler.jsonc dev build already includes --features dev-bootstrap-roots - run: DEPLOY_ENV=dev worker-build --release --features dev-bootstrap-roots + run: DEPLOY_ENV=dev RUSTFLAGS='-Cllvm-args=-wasm-use-legacy-eh' worker-build --release --panic-unwind --features dev-bootstrap-roots # .dev.vars contains the per-log signing and cosigning keys used by # wrangler dev. These are dev-only keys (not production secrets) and are @@ -123,11 +127,12 @@ jobs: # uses worker-build for its wasm build, unlike the CT and MTC jobs # which also compile *_wasm sibling crates that need wasm-pack. - name: Install worker-build - run: cargo install worker-build@0.7.5 --locked + run: cargo install worker-build@0.8.5 --locked + # RUSTFLAGS: force legacy Wasm EH (see ct_worker build comment). - name: Build witness_worker (dev environment) working-directory: crates/witness_worker - run: DEPLOY_ENV=dev worker-build --release + run: DEPLOY_ENV=dev RUSTFLAGS='-Cllvm-args=-wasm-use-legacy-eh' worker-build --release --panic-unwind # .dev.vars contains the WITNESS_SIGNING_KEY used by wrangler dev. This # is a dev-only key (not a production secret) and is committed to the diff --git a/Cargo.lock b/Cargo.lock index c18386a..f7d8d7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,7 +218,7 @@ dependencies = [ "der", "ed25519-dalek", "length_prefixed", - "rand", + "rand 0.10.1", "serde", "serde_with", "sha2", @@ -245,11 +245,13 @@ dependencies = [ "der", "ed25519-dalek", "generic_log_worker", + "getrandom 0.3.4", "getrandom 0.4.2", "jsonschema", "log", "p256", "sct_validator", + "sentry-core", "serde", "serde_json", "serde_with", @@ -339,7 +341,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cpufeatures", - "rand_core", + "rand_core 0.10.0", ] [[package]] @@ -410,9 +412,9 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cmov" -version = "0.5.4" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" [[package]] name = "console_error_panic_hook" @@ -549,7 +551,7 @@ dependencies = [ "getrandom 0.4.2", "hybrid-array", "num-traits", - "rand_core", + "rand_core 0.10.0", "serdect", "subtle", "zeroize", @@ -563,7 +565,7 @@ checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" dependencies = [ "getrandom 0.4.2", "hybrid-array", - "rand_core", + "rand_core 0.10.0", ] [[package]] @@ -574,7 +576,7 @@ checksum = "21f41f23de7d24cdbda7f0c4d9c0351f99a4ceb258ef30e5c1927af8987ffe5a" dependencies = [ "crypto-bigint", "libm", - "rand_core", + "rand_core 0.10.0", ] [[package]] @@ -609,10 +611,12 @@ dependencies = [ "ct_worker_config", "ed25519-dalek", "generic_log_worker", + "getrandom 0.3.4", "getrandom 0.4.2", "jsonschema", "log", "p256", + "sentry-core", "serde", "serde_json", "serde_with", @@ -657,7 +661,7 @@ dependencies = [ "curve25519-dalek-derive", "digest", "fiat-crypto", - "rand_core", + "rand_core 0.10.0", "rustc_version", "subtle", "zeroize", @@ -709,6 +713,16 @@ dependencies = [ "syn", ] +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "serde", + "uuid", +] + [[package]] name = "der" version = "0.8.0" @@ -805,7 +819,7 @@ checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de" dependencies = [ "curve25519-dalek", "ed25519", - "rand_core", + "rand_core 0.10.0", "serde", "sha2", "signature", @@ -834,7 +848,7 @@ dependencies = [ "hybrid-array", "pem-rfc7468", "pkcs8", - "rand_core", + "rand_core 0.10.0", "sec1", "subtle", "zeroize", @@ -897,7 +911,7 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" dependencies = [ - "rand_core", + "rand_core 0.10.0", "subtle", ] @@ -1069,7 +1083,8 @@ dependencies = [ "p256", "parking_lot", "prometheus", - "rand", + "rand 0.10.1", + "sentry-core", "serde", "serde-wasm-bindgen", "serde_bytes", @@ -1105,9 +1120,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1120,7 +1137,7 @@ dependencies = [ "js-sys", "libc", "r-efi 6.0.0", - "rand_core", + "rand_core 0.10.0", "wasip2", "wasip3", "wasm-bindgen", @@ -1139,7 +1156,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" dependencies = [ "ff", - "rand_core", + "rand_core 0.10.0", "subtle", ] @@ -1531,7 +1548,7 @@ dependencies = [ "ml-dsa", "p256", "pkcs8", - "rand", + "rand 0.10.1", "reqwest", "sct_validator", "serde", @@ -1613,11 +1630,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] @@ -2131,6 +2149,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -2150,7 +2177,7 @@ dependencies = [ "crypto-bigint", "crypto-common", "ff", - "rand_core", + "rand_core 0.10.0", "subtle", "zeroize", ] @@ -2233,6 +2260,16 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.1" @@ -2241,7 +2278,26 @@ checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", "getrandom 0.4.2", - "rand_core", + "rand_core 0.10.0", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", ] [[package]] @@ -2420,7 +2476,7 @@ dependencies = [ "digest", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.10.0", "sha2", "signature", "spki", @@ -2606,6 +2662,36 @@ version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +[[package]] +name = "sentry-core" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "545dc562b6758d646ac19e1407f4ebc26d452111386743e03323464bc48bb2e0" +dependencies = [ + "rand 0.9.4", + "sentry-types", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "sentry-types" +version = "0.48.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "041359745a44dd2e14fe21b7510fe7ca8b5beffce6636a0b52e5bc7d5f736887" +dependencies = [ + "debugid", + "hex", + "rand 0.9.4", + "serde", + "serde_json", + "thiserror 2.0.17", + "time", + "url", + "uuid", +] + [[package]] name = "serde" version = "1.0.228" @@ -2790,7 +2876,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" dependencies = [ "digest", - "rand_core", + "rand_core 0.10.0", ] [[package]] @@ -2800,8 +2886,8 @@ dependencies = [ "base64", "criterion", "ed25519-dalek", - "rand", - "rand_core", + "rand 0.10.1", + "rand_core 0.10.0", "sha2", "signature", "thiserror 2.0.17", @@ -2893,6 +2979,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3070,7 +3177,7 @@ version = "0.2.0" dependencies = [ "base64", "ed25519-dalek", - "rand", + "rand 0.10.1", "sha2", "signed_note", "thiserror 2.0.17", @@ -3095,7 +3202,7 @@ dependencies = [ "ed25519-dalek", "length_prefixed", "ml-dsa", - "rand", + "rand 0.10.1", "signed_note", "tlog_checkpoint", "tlog_core", @@ -3346,6 +3453,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "js-sys", + "serde", "wasm-bindgen", ] @@ -3423,9 +3531,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -3436,23 +3544,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3460,9 +3564,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", @@ -3473,9 +3577,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] @@ -3504,9 +3608,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "e7d3be8814f5ba5f074491a469eed3d73c273ffad955f25ed1635efac4b0d269" dependencies = [ "futures-util", "js-sys", @@ -3529,9 +3633,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -3900,6 +4004,7 @@ dependencies = [ "console_log", "ed25519-dalek", "generic_log_worker", + "getrandom 0.3.4", "getrandom 0.4.2", "hex", "jsonschema", @@ -3943,9 +4048,9 @@ dependencies = [ [[package]] name = "worker" -version = "0.7.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "244647fd7673893058f91f22a0eabd0f45bb50298e995688cb0c4b9837081b19" +checksum = "2f8adbf6c9ae45b665dee995c5e3a342c2bd7d58a2e8ca5c75b50ce8b1b8bfd9" dependencies = [ "async-trait", "axum", @@ -3962,6 +4067,7 @@ dependencies = [ "serde-wasm-bindgen", "serde_json", "serde_urlencoded", + "strum", "tokio", "url", "wasm-bindgen", @@ -3974,25 +4080,25 @@ dependencies = [ [[package]] name = "worker-macros" -version = "0.7.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac7e73ffb164183b57bb67d3efb881681fcd93ef5515ba32a4d022c4a6acc2ce" +checksum = "6d908735d273dd7f9c325a842623f4e5a745e0686187ce465b34dc162ad348df" dependencies = [ "async-trait", "proc-macro2", "quote", + "strum", "syn", "wasm-bindgen", - "wasm-bindgen-futures", "wasm-bindgen-macro-support", "worker-sys", ] [[package]] name = "worker-sys" -version = "0.7.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2b96254fcaa9229fd82d886f04be99c4ee8e59c8d80438724aa70039dca838" +checksum = "33faa1a8fa6c7eec67b196e008859c44d468a5ad4f991855cdc856f119e0e98f" dependencies = [ "cfg-if", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index 0c70d36..7c18423 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,12 +73,16 @@ description = "An implementation of the Static Certificate Transparency API on C # transitive dependents; cargo-shear can't see this feature-only edge. A # workspace-level ignore covers every member plus the workspace dependency # itself. -ignored = ["getrandom"] +ignored = ["getrandom", "getrandom_03"] [profile.release] opt-level = "s" # Recommendations from https://developers.cloudflare.com/workers/languages/rust/#binary-size-wasm-opt: -strip = true +# NOTE: `strip = true` is omitted because it strips the `target_features` +# custom section from the .wasm binary, which breaks wasm-bindgen's externref +# transform (required for catch wrappers and panic=unwind support). +# See: https://github.com/cloudflare/workers-rs/issues/1014 +# `wasm-opt` (run automatically by worker-build) handles size optimization. lto = true codegen-units = 1 @@ -110,6 +114,8 @@ ed25519-dalek = { version = "3.0.0", features = ["pem"] } futures-executor = "0.3.31" futures-util = "0.3.31" getrandom = { version = "0.4", features = ["wasm_js"] } +# getrandom 0.3 is needed for sentry-core's transitive dep: sentry-types -> rand 0.9 -> getrandom 0.3 +getrandom_03 = { package = "getrandom", version = "0.3", features = ["wasm_js"] } hex = "0.4" itertools = "0.14.0" jsonschema = "0.30" @@ -125,6 +131,7 @@ parking_lot = "0.12" prometheus = "0.14" rand = "0.10" rand_core = "0.10" +sentry-core = { version = "0.48", default-features = false, features = ["client"] } serde = { version = "1.0", features = ["derive"] } serde-wasm-bindgen = "0.6.5" serde_bytes = "0.11" @@ -147,7 +154,7 @@ tower-service = "0.3.3" reqwest = { version = "0.12", features = ["json"] } url = "2.2" wasm-bindgen = "0.2" -worker = "0.7.4" +worker = "0.8.5" x509-cert = "0.3.0" x509_util = { path = "crates/x509_util" } diff --git a/crates/bootstrap_mtc_worker/Cargo.toml b/crates/bootstrap_mtc_worker/Cargo.toml index 9e0e246..e3dba8c 100644 --- a/crates/bootstrap_mtc_worker/Cargo.toml +++ b/crates/bootstrap_mtc_worker/Cargo.toml @@ -46,7 +46,9 @@ der.workspace = true generic_log_worker.workspace = true ed25519-dalek.workspace = true getrandom.workspace = true +getrandom_03.workspace = true log.workspace = true +sentry-core.workspace = true p256.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/bootstrap_mtc_worker/src/batcher_do.rs b/crates/bootstrap_mtc_worker/src/batcher_do.rs index 4259358..6ef14ac 100644 --- a/crates/bootstrap_mtc_worker/src/batcher_do.rs +++ b/crates/bootstrap_mtc_worker/src/batcher_do.rs @@ -1,4 +1,4 @@ -use crate::{BootstrapMtcSequenceMetadata, CONFIG}; +use crate::{BootstrapMtcSequenceMetadata, CONFIG, init_sentry}; use generic_log_worker::{BATCHER_BINDING, BatcherConfig, GenericBatcher, get_durable_object_name}; #[allow(clippy::wildcard_imports)] use worker::*; @@ -6,6 +6,10 @@ use worker::*; #[durable_object(fetch)] struct Batcher(GenericBatcher); +// SAFETY: Durable Objects are single-threaded; the `RefUnwindSafe` bound +// is required by `wasm-bindgen` when building with `panic=unwind`. +impl std::panic::RefUnwindSafe for Batcher {} + impl DurableObject for Batcher { fn new(state: State, env: Env) -> Self { let name = get_durable_object_name( @@ -25,10 +29,15 @@ impl DurableObject for Batcher { enable_dedup: false, // deduplication is not currently supported location_hint: params.location_hint.clone(), }; + init_sentry(&env); Batcher(GenericBatcher::new(state, env, config)) } async fn fetch(&self, req: Request) -> Result { - self.0.fetch(req).await + generic_log_worker::obs::sentry::catch_unwind_report_and_flush( + &[("handler", "do_fetch"), ("do_type", "batcher")], + self.0.fetch(req), + ) + .await } } diff --git a/crates/bootstrap_mtc_worker/src/ccadb_roots_cron.rs b/crates/bootstrap_mtc_worker/src/ccadb_roots_cron.rs index 7ccb330..a73da07 100644 --- a/crates/bootstrap_mtc_worker/src/ccadb_roots_cron.rs +++ b/crates/bootstrap_mtc_worker/src/ccadb_roots_cron.rs @@ -24,31 +24,49 @@ pub(crate) const CCADB_ROOTS_FILENAME: &str = "mtc_bootstrap_roots.pem"; #[event(scheduled)] async fn main(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) { - // Update CCADB roots - log::info!("Updating CCADB roots"); - match env.kv(CCADB_ROOTS_NAMESPACE) { - Ok(kv) => { - if let Err(e) = update_ccadb_roots(&kv).await { - log::warn!("Failed to update CCADB roots: {e}"); - } else { - log::info!("Successfully updated CCADB roots"); + crate::init_sentry(&env); + generic_log_worker::obs::sentry::catch_unwind_and_flush(async { + // Update CCADB roots + log::info!("Updating CCADB roots"); + match env.kv(CCADB_ROOTS_NAMESPACE) { + Ok(kv) => { + if let Err(e) = update_ccadb_roots(&kv).await { + log::warn!("Failed to update CCADB roots: {e}"); + generic_log_worker::obs::sentry::capture_error_and_flush( + &e, + sentry_core::Level::Error, + &[("handler", "scheduled"), ("cron", "ccadb_roots")], + ) + .await; + } else { + log::info!("Successfully updated CCADB roots"); + } } + Err(e) => log::warn!("Failed to get KV namespace '{CCADB_ROOTS_NAMESPACE}': {e}"), } - Err(e) => log::warn!("Failed to get KV namespace '{CCADB_ROOTS_NAMESPACE}': {e}"), - } - // Update CT logs for SCT validation - log::info!("Updating CT logs"); - match env.kv(crate::ct_logs_cron::CT_LOGS_NAMESPACE) { - Ok(kv) => match crate::ct_logs_cron::update_ct_logs(&kv).await { - Ok(()) => log::info!("Successfully updated CT logs"), - Err(e) => log::warn!("Failed to update CT logs: {e}"), - }, - Err(e) => log::warn!( - "Failed to get KV namespace '{}': {e}", - crate::ct_logs_cron::CT_LOGS_NAMESPACE - ), - } + // Update CT logs for SCT validation + log::info!("Updating CT logs"); + match env.kv(crate::ct_logs_cron::CT_LOGS_NAMESPACE) { + Ok(kv) => match crate::ct_logs_cron::update_ct_logs(&kv).await { + Ok(()) => log::info!("Successfully updated CT logs"), + Err(e) => { + log::warn!("Failed to update CT logs: {e}"); + generic_log_worker::obs::sentry::capture_error_and_flush( + &e, + sentry_core::Level::Error, + &[("handler", "scheduled"), ("cron", "ct_logs")], + ) + .await; + } + }, + Err(e) => log::warn!( + "Failed to get KV namespace '{}': {e}", + crate::ct_logs_cron::CT_LOGS_NAMESPACE + ), + } + }) + .await; } /// Update CCADB roots at each of the provided keys. Roots are pruned to match diff --git a/crates/bootstrap_mtc_worker/src/cleaner_do.rs b/crates/bootstrap_mtc_worker/src/cleaner_do.rs index 1eb99a5..6c670a2 100644 --- a/crates/bootstrap_mtc_worker/src/cleaner_do.rs +++ b/crates/bootstrap_mtc_worker/src/cleaner_do.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use crate::{CONFIG, load_checkpoint_cosigner, load_origin}; +use crate::{CONFIG, init_sentry, load_checkpoint_cosigner, load_origin}; use bootstrap_mtc_api::BootstrapMtcPendingLogEntry; use generic_log_worker::{CLEANER_BINDING, CleanerConfig, GenericCleaner, get_durable_object_name}; use signed_note::VerifierList; @@ -12,6 +12,10 @@ use worker::*; #[durable_object(alarm)] struct Cleaner(GenericCleaner); +// SAFETY: Durable Objects are single-threaded; the `RefUnwindSafe` bound +// is required by `wasm-bindgen` when building with `panic=unwind`. +impl std::panic::RefUnwindSafe for Cleaner {} + impl DurableObject for Cleaner { fn new(state: State, env: Env) -> Self { let name = get_durable_object_name( @@ -31,14 +35,23 @@ impl DurableObject for Cleaner { clean_interval: Duration::from_secs(params.clean_interval_secs), }; + init_sentry(&env); Cleaner(GenericCleaner::new(state, &env, config)) } async fn fetch(&self, req: Request) -> Result { - self.0.fetch(req).await + generic_log_worker::obs::sentry::catch_unwind_report_and_flush( + &[("handler", "do_fetch"), ("do_type", "cleaner")], + self.0.fetch(req), + ) + .await } async fn alarm(&self) -> Result { - self.0.alarm().await + generic_log_worker::obs::sentry::catch_unwind_report_and_flush( + &[("handler", "do_alarm"), ("do_type", "cleaner")], + self.0.alarm(), + ) + .await } } diff --git a/crates/bootstrap_mtc_worker/src/frontend_worker.rs b/crates/bootstrap_mtc_worker/src/frontend_worker.rs index 1138592..daf51a7 100644 --- a/crates/bootstrap_mtc_worker/src/frontend_worker.rs +++ b/crates/bootstrap_mtc_worker/src/frontend_worker.rs @@ -4,7 +4,8 @@ //! Entrypoint for the static CT submission APIs. use crate::{ - BootstrapMtcSequenceMetadata, CONFIG, load_checkpoint_cosigner, load_origin, load_roots, + BootstrapMtcSequenceMetadata, CONFIG, init_sentry, load_checkpoint_cosigner, load_origin, + load_roots, }; use bootstrap_mtc_api::{ AddEntryRequest, AddEntryResponse, BootstrapMtcLogEntry, GetRootsResponse, @@ -100,23 +101,32 @@ async fn main( env: Env, ctx: Context, ) -> Result> { + init_sentry(&env); let wshim = Wshim::from_env(&env); let registry = metrics::registry(); - let response = Router::new() - .route("/logs/{log}/get-roots", get(get_roots)) - .route("/logs/{log}/add-entry", post(add_entry)) - .route("/logs/{log}/get-certificate", post(get_certificate)) - .route("/logs/{log}/get-landmark-bundle", get(get_landmark_bundle)) - .route("/logs/{log}/metadata", get(metadata)) - .route("/logs/{log}/sequencer_id", get(sequencer_id)) - .route("/logs/{log}/{*key}", get(get_object)) - .layer(middleware::from_fn_with_state( - (env.clone(), metrics::FrontendWorkerMetrics::new(®istry)), - request_metrics, - )) - .with_state(env) - .call(req) - .await?; + let response = generic_log_worker::obs::sentry::catch_unwind_and_flush(async { + Router::new() + .route("/logs/{log}/get-roots", get(get_roots)) + .route("/logs/{log}/add-entry", post(add_entry)) + .route("/logs/{log}/get-certificate", post(get_certificate)) + .route("/logs/{log}/get-landmark-bundle", get(get_landmark_bundle)) + .route("/logs/{log}/metadata", get(metadata)) + .route("/logs/{log}/sequencer_id", get(sequencer_id)) + .route("/logs/{log}/{*key}", get(get_object)) + .layer(middleware::from_fn_with_state( + (env.clone(), metrics::FrontendWorkerMetrics::new(®istry)), + request_metrics, + )) + .with_state(env) + .call(req) + .await + }) + .await?; + // Flush any buffered Sentry envelopes (e.g. from panics caught by + // catch_unwind_and_flush — the panic hook captures the event, and + // catch_unwind_and_flush ensures the transport is flushed before + // re-panicking). This explicit flush handles non-panic error paths. + generic_log_worker::obs::sentry::flush().await; if let Ok(wshim) = wshim { ctx.wait_until(async move { wshim.flush(&generic_log_worker::obs::logs::LOGGER).await; diff --git a/crates/bootstrap_mtc_worker/src/lib.rs b/crates/bootstrap_mtc_worker/src/lib.rs index 516e945..42e167d 100644 --- a/crates/bootstrap_mtc_worker/src/lib.rs +++ b/crates/bootstrap_mtc_worker/src/lib.rs @@ -35,6 +35,16 @@ static CONFIG: LazyLock = LazyLock::new(|| { static SIGNING_KEY_MAP: OnceLock>> = OnceLock::new(); static ROOTS: OnceLock = OnceLock::new(); +/// Initialize sentry from the `SENTRY_DSN` environment variable. +/// +/// Does nothing when the variable is absent or empty, allowing deployments +/// without sentry support. +pub(crate) fn init_sentry(env: &Env) { + if let Ok(dsn) = env.var("SENTRY_DSN") { + let _ = generic_log_worker::obs::sentry::init(&dsn.to_string(), env!("DEPLOY_ENV")); + } +} + pub(crate) fn load_signing_key(env: &Env, name: &str) -> Result<&'static Ed25519SigningKey> { load_ed25519_key(env, name, &SIGNING_KEY_MAP, &format!("SIGNING_KEY_{name}")) } diff --git a/crates/bootstrap_mtc_worker/src/sequencer_do.rs b/crates/bootstrap_mtc_worker/src/sequencer_do.rs index 9439464..23f12b8 100644 --- a/crates/bootstrap_mtc_worker/src/sequencer_do.rs +++ b/crates/bootstrap_mtc_worker/src/sequencer_do.rs @@ -5,7 +5,9 @@ use std::{collections::VecDeque, time::Duration}; -use crate::{BootstrapMtcSequenceMetadata, CONFIG, load_checkpoint_cosigner, load_origin}; +use crate::{ + BootstrapMtcSequenceMetadata, CONFIG, init_sentry, load_checkpoint_cosigner, load_origin, +}; use bootstrap_mtc_api::{ BootstrapMtcLogEntry, LANDMARK_BUNDLE_KEY, LANDMARK_CHECKPOINT_KEY, LANDMARK_KEY, LandmarkSequence, @@ -26,6 +28,10 @@ use worker::*; #[durable_object(alarm)] struct Sequencer(GenericSequencer); +// SAFETY: Durable Objects are single-threaded; the `RefUnwindSafe` bound +// is required by `wasm-bindgen` when building with `panic=unwind`. +impl std::panic::RefUnwindSafe for Sequencer {} + impl DurableObject for Sequencer { fn new(state: State, env: Env) -> Self { let name = get_durable_object_name( @@ -49,15 +55,24 @@ impl DurableObject for Sequencer { checkpoint_callback: checkpoint_callback(&env, name), }; + init_sentry(&env); Sequencer(GenericSequencer::new(state, env, config)) } async fn fetch(&self, req: Request) -> Result { - self.0.fetch(req).await + generic_log_worker::obs::sentry::catch_unwind_report_and_flush( + &[("handler", "do_fetch"), ("do_type", "sequencer")], + self.0.fetch(req), + ) + .await } async fn alarm(&self) -> Result { - self.0.alarm().await + generic_log_worker::obs::sentry::catch_unwind_report_and_flush( + &[("handler", "do_alarm"), ("do_type", "sequencer")], + self.0.alarm(), + ) + .await } } diff --git a/crates/bootstrap_mtc_worker/wrangler.jsonc b/crates/bootstrap_mtc_worker/wrangler.jsonc index 8a1ceac..826ca9d 100644 --- a/crates/bootstrap_mtc_worker/wrangler.jsonc +++ b/crates/bootstrap_mtc_worker/wrangler.jsonc @@ -19,7 +19,10 @@ // config and roots. Add `--features dev-bootstrap-roots` to the // worker-build command to enable dev bootstrap roots (which // should NOT be enabled in a production deployment). - "command": "cargo install -q worker-build@0.7.5 && DEPLOY_ENV=dev worker-build --release --features dev-bootstrap-roots" + // RUSTFLAGS: force legacy Wasm EH so wasm-bindgen 0.2.126's + // wasmparser accepts the module (modern exnref opcodes from + // nightly ≥2026-05-09 are not yet supported). + "command": "cargo install -q worker-build@0.8.5 && DEPLOY_ENV=dev RUSTFLAGS='-Cllvm-args=-wasm-use-legacy-eh' worker-build --release --panic-unwind --features dev-bootstrap-roots" }, "version_metadata": { "binding": "VERSION_METADATA" @@ -78,7 +81,8 @@ "build": { // Change '--release' to '--dev' to compile with debug symbols. // DEPLOY_ENV is used in build.rs to select per-environment config and roots. - "command": "cargo install -q worker-build@0.7.5 && DEPLOY_ENV=bootstrap-mtca worker-build --release" + // RUSTFLAGS: force legacy Wasm EH (see dev env comment). + "command": "cargo install -q worker-build@0.8.5 && DEPLOY_ENV=bootstrap-mtca RUSTFLAGS='-Cllvm-args=-wasm-use-legacy-eh' worker-build --release --panic-unwind" }, "route": { "pattern": "bootstrap-mtca.cloudflareresearch.com", diff --git a/crates/ct_worker/Cargo.toml b/crates/ct_worker/Cargo.toml index 7cc4f49..b3725b5 100644 --- a/crates/ct_worker/Cargo.toml +++ b/crates/ct_worker/Cargo.toml @@ -37,7 +37,9 @@ config = { path = "./config", package = "ct_worker_config" } generic_log_worker.workspace = true ed25519-dalek.workspace = true getrandom.workspace = true +getrandom_03.workspace = true log.workspace = true +sentry-core.workspace = true p256.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/ct_worker/src/batcher_do.rs b/crates/ct_worker/src/batcher_do.rs index 95d8f0b..d20a502 100644 --- a/crates/ct_worker/src/batcher_do.rs +++ b/crates/ct_worker/src/batcher_do.rs @@ -1,4 +1,4 @@ -use crate::{CONFIG, StaticCTSequenceMetadata}; +use crate::{CONFIG, StaticCTSequenceMetadata, init_sentry}; use generic_log_worker::{BATCHER_BINDING, BatcherConfig, GenericBatcher, get_durable_object_name}; #[allow(clippy::wildcard_imports)] use worker::*; @@ -6,6 +6,10 @@ use worker::*; #[durable_object(fetch)] struct Batcher(GenericBatcher); +// SAFETY: Durable Objects are single-threaded; the `RefUnwindSafe` bound +// is required by `wasm-bindgen` when building with `panic=unwind`. +impl std::panic::RefUnwindSafe for Batcher {} + impl DurableObject for Batcher { fn new(state: State, env: Env) -> Self { let name = get_durable_object_name( @@ -25,10 +29,15 @@ impl DurableObject for Batcher { enable_dedup: params.enable_dedup, location_hint: params.location_hint.clone(), }; + init_sentry(&env); Batcher(GenericBatcher::new(state, env, config)) } async fn fetch(&self, req: Request) -> Result { - self.0.fetch(req).await + generic_log_worker::obs::sentry::catch_unwind_report_and_flush( + &[("handler", "do_fetch"), ("do_type", "batcher")], + self.0.fetch(req), + ) + .await } } diff --git a/crates/ct_worker/src/ccadb_roots_cron.rs b/crates/ct_worker/src/ccadb_roots_cron.rs index 7697344..57f180f 100644 --- a/crates/ct_worker/src/ccadb_roots_cron.rs +++ b/crates/ct_worker/src/ccadb_roots_cron.rs @@ -17,7 +17,7 @@ use x509_cert::{ }; use x509_util::CertPool; -use crate::CONFIG; +use crate::{CONFIG, init_sentry}; // A KV namespace with this binding must be configured in 'wrangler.jsonc' if // any log shards have 'enable_ccadb_roots=true'. @@ -29,25 +29,35 @@ pub(crate) fn ccadb_roots_filename(name: &str) -> String { #[event(scheduled)] async fn main(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) { - let mut keys = Vec::new(); - for (name, params) in &CONFIG.logs { - if params.enable_ccadb_roots { - keys.push(ccadb_roots_filename(name)); + init_sentry(&env); + generic_log_worker::obs::sentry::catch_unwind_and_flush(async { + let mut keys = Vec::new(); + for (name, params) in &CONFIG.logs { + if params.enable_ccadb_roots { + keys.push(ccadb_roots_filename(name)); + } } - } - log::info!("Updating CCADB roots for keys: {keys:?}"); - let kv = match env.kv(CCADB_ROOTS_NAMESPACE) { - Ok(kv) => kv, - Err(e) => { - log::warn!("Failed to get KV namespace '{CCADB_ROOTS_NAMESPACE}': {e}"); - return; + log::info!("Updating CCADB roots for keys: {keys:?}"); + let kv = match env.kv(CCADB_ROOTS_NAMESPACE) { + Ok(kv) => kv, + Err(e) => { + log::warn!("Failed to get KV namespace '{CCADB_ROOTS_NAMESPACE}': {e}"); + return; + } + }; + if let Err(e) = update_ccadb_roots(&keys, &kv).await { + log::warn!("Failed to update CCADB roots: {e}"); + generic_log_worker::obs::sentry::capture_error_and_flush( + &e, + sentry_core::Level::Error, + &[("handler", "scheduled"), ("cron", "ccadb_roots")], + ) + .await; + } else { + log::info!("Successfully updated CCADB roots"); } - }; - if let Err(e) = update_ccadb_roots(&keys, &kv).await { - log::warn!("Failed to update CCADB roots: {e}"); - } else { - log::info!("Successfully updated CCADB roots"); - } + }) + .await; } /// Update CCADB roots at each of the provided keys. Roots are never removed diff --git a/crates/ct_worker/src/cleaner_do.rs b/crates/ct_worker/src/cleaner_do.rs index 8943441..726a5d4 100644 --- a/crates/ct_worker/src/cleaner_do.rs +++ b/crates/ct_worker/src/cleaner_do.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use crate::{CONFIG, load_checkpoint_signers, load_origin}; +use crate::{CONFIG, init_sentry, load_checkpoint_signers, load_origin}; use generic_log_worker::{CLEANER_BINDING, CleanerConfig, GenericCleaner, get_durable_object_name}; use signed_note::VerifierList; use static_ct_api::StaticCTPendingLogEntry; @@ -11,6 +11,10 @@ use worker::*; #[durable_object(alarm)] struct Cleaner(GenericCleaner); +// SAFETY: Durable Objects are single-threaded; the `RefUnwindSafe` bound +// is required by `wasm-bindgen` when building with `panic=unwind`. +impl std::panic::RefUnwindSafe for Cleaner {} + impl DurableObject for Cleaner { fn new(state: State, env: Env) -> Self { let name = get_durable_object_name( @@ -35,14 +39,23 @@ impl DurableObject for Cleaner { clean_interval: Duration::from_secs(params.clean_interval_secs), }; + init_sentry(&env); Cleaner(GenericCleaner::new(state, &env, config)) } async fn fetch(&self, req: Request) -> Result { - self.0.fetch(req).await + generic_log_worker::obs::sentry::catch_unwind_report_and_flush( + &[("handler", "do_fetch"), ("do_type", "cleaner")], + self.0.fetch(req), + ) + .await } async fn alarm(&self) -> Result { - self.0.alarm().await + generic_log_worker::obs::sentry::catch_unwind_report_and_flush( + &[("handler", "do_alarm"), ("do_type", "cleaner")], + self.0.alarm(), + ) + .await } } diff --git a/crates/ct_worker/src/frontend_worker.rs b/crates/ct_worker/src/frontend_worker.rs index ca3c6c1..d1ca065 100644 --- a/crates/ct_worker/src/frontend_worker.rs +++ b/crates/ct_worker/src/frontend_worker.rs @@ -3,7 +3,7 @@ //! Entrypoint for the static CT submission APIs. -use crate::{CONFIG, StaticCTSequenceMetadata, load_roots, load_signing_key}; +use crate::{CONFIG, StaticCTSequenceMetadata, init_sentry, load_roots, load_signing_key}; use config::{LogType, TemporalInterval}; use generic_log_worker::{ ENTRY_ENDPOINT, ObjectBucket, batcher_id_from_lookup_key, deserialize, @@ -76,22 +76,31 @@ async fn main( env: Env, ctx: Context, ) -> Result> { + init_sentry(&env); let wshim = Wshim::from_env(&env); let registry = metrics::registry(); - let response = Router::new() - .route("/logs/{log}/ct/v1/get-roots", get(get_roots)) - .route("/logs/{log}/ct/v1/add-chain", post(add_chain)) - .route("/logs/{log}/ct/v1/add-pre-chain", post(add_pre_chain)) - .route("/logs/{log}/log.v3.json", get(log_v3_json)) - .route("/logs/{log}/sequencer_id", get(sequencer_id)) - .route("/logs/{log}/{*key}", get(get_object)) - .layer(middleware::from_fn_with_state( - (env.clone(), metrics::FrontendWorkerMetrics::new(®istry)), - request_metrics, - )) - .with_state(env) - .call(req) - .await?; + let response = generic_log_worker::obs::sentry::catch_unwind_and_flush(async { + Router::new() + .route("/logs/{log}/ct/v1/get-roots", get(get_roots)) + .route("/logs/{log}/ct/v1/add-chain", post(add_chain)) + .route("/logs/{log}/ct/v1/add-pre-chain", post(add_pre_chain)) + .route("/logs/{log}/log.v3.json", get(log_v3_json)) + .route("/logs/{log}/sequencer_id", get(sequencer_id)) + .route("/logs/{log}/{*key}", get(get_object)) + .layer(middleware::from_fn_with_state( + (env.clone(), metrics::FrontendWorkerMetrics::new(®istry)), + request_metrics, + )) + .with_state(env) + .call(req) + .await + }) + .await?; + // Flush any buffered Sentry envelopes (e.g. from panics caught by + // catch_unwind_and_flush — the panic hook captures the event, and + // catch_unwind_and_flush ensures the transport is flushed before + // re-panicking). This explicit flush handles non-panic error paths. + generic_log_worker::obs::sentry::flush().await; if let Ok(wshim) = wshim { ctx.wait_until(async move { wshim.flush(&generic_log_worker::obs::logs::LOGGER).await; diff --git a/crates/ct_worker/src/lib.rs b/crates/ct_worker/src/lib.rs index d110adb..a350f08 100644 --- a/crates/ct_worker/src/lib.rs +++ b/crates/ct_worker/src/lib.rs @@ -33,6 +33,16 @@ static CONFIG: LazyLock = LazyLock::new(|| { static SIGNING_KEY_MAP: OnceLock>> = OnceLock::new(); static WITNESS_KEY_MAP: OnceLock>> = OnceLock::new(); +/// Initialize sentry from the `SENTRY_DSN` environment variable. +/// +/// Does nothing when the variable is absent or empty, allowing deployments +/// without sentry support. +pub(crate) fn init_sentry(env: &Env) { + if let Ok(dsn) = env.var("SENTRY_DSN") { + let _ = generic_log_worker::obs::sentry::init(&dsn.to_string(), env!("DEPLOY_ENV")); + } +} + pub(crate) fn load_signing_key(env: &Env, name: &str) -> Result<&'static EcdsaSigningKey> { let once = &SIGNING_KEY_MAP.get_or_init(|| { CONFIG diff --git a/crates/ct_worker/src/sequencer_do.rs b/crates/ct_worker/src/sequencer_do.rs index 6763ec7..83c3216 100644 --- a/crates/ct_worker/src/sequencer_do.rs +++ b/crates/ct_worker/src/sequencer_do.rs @@ -5,7 +5,7 @@ use std::time::Duration; -use crate::{CONFIG, StaticCTSequenceMetadata, load_checkpoint_signers, load_origin}; +use crate::{CONFIG, StaticCTSequenceMetadata, init_sentry, load_checkpoint_signers, load_origin}; use generic_log_worker::{ GenericSequencer, SEQUENCER_BINDING, SequencerConfig, empty_checkpoint_callback, get_durable_object_name, @@ -17,6 +17,10 @@ use worker::*; #[durable_object(alarm)] struct Sequencer(GenericSequencer); +// SAFETY: Durable Objects are single-threaded; the `RefUnwindSafe` bound +// is required by `wasm-bindgen` when building with `panic=unwind`. +impl std::panic::RefUnwindSafe for Sequencer {} + impl DurableObject for Sequencer { fn new(state: State, env: Env) -> Self { let name = get_durable_object_name( @@ -40,14 +44,23 @@ impl DurableObject for Sequencer { checkpoint_callback: empty_checkpoint_callback(), }; + init_sentry(&env); Sequencer(GenericSequencer::new(state, env, config)) } async fn fetch(&self, req: Request) -> Result { - self.0.fetch(req).await + generic_log_worker::obs::sentry::catch_unwind_report_and_flush( + &[("handler", "do_fetch"), ("do_type", "sequencer")], + self.0.fetch(req), + ) + .await } async fn alarm(&self) -> Result { - self.0.alarm().await + generic_log_worker::obs::sentry::catch_unwind_report_and_flush( + &[("handler", "do_alarm"), ("do_type", "sequencer")], + self.0.alarm(), + ) + .await } } diff --git a/crates/ct_worker/wrangler.jsonc b/crates/ct_worker/wrangler.jsonc index cae3280..9b92527 100644 --- a/crates/ct_worker/wrangler.jsonc +++ b/crates/ct_worker/wrangler.jsonc @@ -17,7 +17,10 @@ "build": { // Change '--release' to '--dev' to compile with debug symbols. // DEPLOY_ENV is used in build.rs to select per-environment config and roots. - "command": "cargo install -q worker-build@0.7.5 && DEPLOY_ENV=dev worker-build --release" + // RUSTFLAGS: force legacy Wasm EH so wasm-bindgen 0.2.126's + // wasmparser accepts the module (modern exnref opcodes from + // nightly ≥2026-05-09 are not yet supported). + "command": "cargo install -q worker-build@0.8.5 && DEPLOY_ENV=dev RUSTFLAGS='-Cllvm-args=-wasm-use-legacy-eh' worker-build --release --panic-unwind" }, "route": { "pattern": "static-ct-dev.cloudflareresearch.com", @@ -132,7 +135,8 @@ "build": { // Change '--release' to '--dev' to compile with debug symbols. // DEPLOY_ENV is used in build.rs to select per-environment config and roots. - "command": "cargo install -q worker-build@0.7.5 && DEPLOY_ENV=cftest worker-build --release" + // RUSTFLAGS: force legacy Wasm EH (see dev env comment). + "command": "cargo install -q worker-build@0.8.5 && DEPLOY_ENV=cftest RUSTFLAGS='-Cllvm-args=-wasm-use-legacy-eh' worker-build --release --panic-unwind" }, "route": { "pattern": "static-ct.cloudflareresearch.com", @@ -216,7 +220,8 @@ "build": { // Change '--release' to '--dev' to compile with debug symbols. // DEPLOY_ENV is used in build.rs to select per-environment config and roots. - "command": "cargo install -q worker-build@0.7.5 && DEPLOY_ENV=raio worker-build --release" + // RUSTFLAGS: force legacy Wasm EH (see dev env comment). + "command": "cargo install -q worker-build@0.8.5 && DEPLOY_ENV=raio RUSTFLAGS='-Cllvm-args=-wasm-use-legacy-eh' worker-build --release --panic-unwind" }, "routes": [ "https://ct.cloudflare.com/logs/raio2025h2b/*", diff --git a/crates/generic_log_worker/Cargo.toml b/crates/generic_log_worker/Cargo.toml index 7f7f48c..8470334 100644 --- a/crates/generic_log_worker/Cargo.toml +++ b/crates/generic_log_worker/Cargo.toml @@ -34,6 +34,7 @@ log = { workspace = true, features = ["kv"] } p256.workspace = true prometheus.workspace = true rand.workspace = true +sentry-core.workspace = true serde-wasm-bindgen.workspace = true serde.workspace = true serde_bytes.workspace = true diff --git a/crates/generic_log_worker/src/obs/mod.rs b/crates/generic_log_worker/src/obs/mod.rs index 40fc7d0..2dcf7f1 100644 --- a/crates/generic_log_worker/src/obs/mod.rs +++ b/crates/generic_log_worker/src/obs/mod.rs @@ -2,6 +2,7 @@ use axum::http::header; pub mod logs; pub mod metrics; +pub mod sentry; #[derive(Clone)] pub struct Wshim { diff --git a/crates/generic_log_worker/src/obs/sentry.rs b/crates/generic_log_worker/src/obs/sentry.rs new file mode 100644 index 0000000..798af20 --- /dev/null +++ b/crates/generic_log_worker/src/obs/sentry.rs @@ -0,0 +1,389 @@ +// Copyright (c) 2025 Cloudflare, Inc. +// Licensed under the BSD-3-Clause license found in the LICENSE file or at https://opensource.org/licenses/BSD-3-Clause + +//! Sentry integration for Cloudflare Workers. +//! +//! Provides a custom [`sentry_core::Transport`] that buffers envelopes in memory +//! and flushes them synchronously via the Workers `fetch` API. Flushing is +//! intended only for catastrophic errors where latency no longer matters. +//! +//! # Usage +//! +//! ```ignore +//! // In your worker crate's lib.rs: +//! static SENTRY_TRANSPORT: OnceLock>> = OnceLock::new(); +//! +//! pub fn init_sentry(env: &Env) -> Option<&'static Arc> { +//! SENTRY_TRANSPORT.get_or_init(|| { +//! let dsn = env.var("SENTRY_DSN").ok()?.to_string(); +//! if dsn.is_empty() { return None; } +//! obs::sentry::init(&dsn, env!("DEPLOY_ENV")) +//! }).as_ref() +//! } +//! +//! // On catastrophic error: +//! sentry_core::capture_message("something broke", sentry_core::Level::Fatal); +//! if let Some(transport) = init_sentry(&env) { +//! obs::sentry::flush(transport).await; +//! } +//! ``` + +use std::panic; +use std::sync::{Arc, Mutex, OnceLock}; + +use std::borrow::Cow; +use std::time::UNIX_EPOCH; + +use sentry_core::protocol::{Event, Exception, Frame, Level, Mechanism, Stacktrace}; +use sentry_core::{ClientOptions, Envelope, Transport, TransportFactory}; +use sha2::{Digest, Sha256}; + +/// A [`Transport`] that buffers envelopes in memory for later synchronous +/// flush via Workers `fetch`. Serialization is deferred to flush time to avoid +/// wasted work when no flush occurs. +pub struct WorkerTransport { + dsn: sentry_core::types::Dsn, + envelopes: Mutex>, +} + +impl Transport for &'static WorkerTransport { + fn send_envelope(&self, envelope: Envelope) { + if let Ok(mut guard) = self.envelopes.lock() { + guard.push(envelope); + } + } +} + +struct Factory(&'static WorkerTransport); + +impl TransportFactory for Factory { + fn create_transport(&self, _opts: &ClientOptions) -> Arc { + Arc::new(self.0) + } +} + +/// Global transport reference for the panic hook. The hook cannot go through +/// `sentry_core::capture_event` because that calls `random_uuid()` which +/// requires `getrandom` — unavailable during a WASM panic. +static PANIC_HOOK_TRANSPORT: OnceLock = OnceLock::new(); + +/// Initialize sentry with a custom Workers-compatible transport. +/// +/// Installs a panic hook that captures panics as Sentry exception events, +/// chaining with any previously installed hook (e.g. `console_error_panic_hook`) +/// so that console output is preserved. The hook bypasses +/// `sentry_core::capture_event` entirely because that function calls +/// `random_uuid()` -> `getrandom`, which panics in WASM during a panic hook +/// (JS interop unavailable). Instead, we construct the [`Envelope`] directly +/// and push it to the transport buffer. +/// +/// Returns `None` if the DSN is empty or invalid, allowing graceful opt-out +/// when `SENTRY_DSN` is not configured. +#[must_use] +#[allow(clippy::too_many_lines)] // Panic-hook event construction is deliberately inline; see safety comments. +pub fn init(dsn: &str, environment: &str) -> Option<&'static WorkerTransport> { + use std::sync::Once; + static HOOK: Once = Once::new(); + + if dsn.is_empty() { + return None; + } + let parsed_dsn: sentry_core::types::Dsn = dsn.parse().ok()?; + // Store a reference for the panic hook before installing it. + let transport = PANIC_HOOK_TRANSPORT.get_or_init(|| WorkerTransport { + dsn: parsed_dsn.clone(), + envelopes: Mutex::default(), + }); + let client = sentry_core::Client::from(ClientOptions { + dsn: Some(parsed_dsn), + environment: Some(environment.to_string().into()), + release: sentry_core::release_name!(), + transport: Some(Arc::new(Factory(transport))), + ..Default::default() + }); + let hub = sentry_core::Hub::current(); + hub.bind_client(Some(Arc::new(client))); + + // Install the panic hook (at most once). + HOOK.call_once(|| { + let next = panic::take_hook(); + panic::set_hook(Box::new(move |info| { + // Build a Sentry Event from the panic info. Since WASM + // targets do not support `std::backtrace`, we extract + // what we can from the panic location (file, line, column) + // and encode it as a single-frame stacktrace. + // + // This code must not call `Event::default()` or any path + // through `getrandom` (e.g. `Uuid::new_v4()`), because the + // JS interop required by `getrandom`'s `wasm_js` backend + // may not be available during a panic hook, causing a + // double-panic that aborts the WASM module. + // + // `SystemTime::now()` also panics on wasm32-unknown-unknown + // (no clock), so we use `UNIX_EPOCH` — the Sentry ingest + // endpoint uses the `sent_at` header instead when the event + // timestamp is epoch-zero. + let msg = match info.payload().downcast_ref::<&'static str>() { + Some(s) => (*s).to_string(), + None => match info.payload().downcast_ref::() { + Some(s) => s.clone(), + None => "Box".to_string(), + }, + }; + + // Derive a deterministic UUID from the panic message and + // location so that Sentry does not deduplicate distinct + // panics (which it would if every event shared Uuid::nil). + // SHA-256 is pure Rust, so it is safe to call from a panic + // hook — unlike `getrandom`/`Uuid::new_v4()`. + let event_id = { + let mut h = Sha256::new(); + h.update(msg.as_bytes()); + if let Some(loc) = info.location() { + h.update(loc.file().as_bytes()); + h.update(loc.line().to_le_bytes()); + h.update(loc.column().to_le_bytes()); + } + let hash = h.finalize(); + let mut bytes = [0u8; 16]; + bytes.copy_from_slice(&hash[..16]); + // Set version (4) and variant (RFC 4122) bits so the + // value is a syntactically valid UUID. + bytes[6] = (bytes[6] & 0x0F) | 0x40; // version 4 + bytes[8] = (bytes[8] & 0x3F) | 0x80; // variant RFC 4122 + sentry_core::types::Uuid::from_bytes(bytes) + }; + + let stacktrace = info.location().map(|loc| Stacktrace { + frames: vec![Frame { + function: Some("panic".into()), + filename: Some(loc.file().into()), + lineno: Some(loc.line().into()), + colno: Some(loc.column().into()), + ..Default::default() + }], + ..Default::default() + }); + + // Construct the event field-by-field instead of using + // `..Default::default()` because `Event::default()` calls + // `random_uuid()` and `SystemTime::now()`, both of which + // panic in WASM panic hooks. The Sentry ingest endpoint + // uses the `sent_at` envelope header when the event + // timestamp is epoch-zero. + #[allow(clippy::default_trait_access)] + let event = Event { + event_id, + level: Level::Fatal, + fingerprint: Cow::Borrowed(&[]), + platform: Cow::Borrowed("other"), + timestamp: UNIX_EPOCH, + exception: vec![Exception { + ty: "panic".into(), + value: Some(msg), + mechanism: Some(Mechanism { + ty: "panic".into(), + handled: Some(false), + ..Default::default() + }), + stacktrace, + ..Default::default() + }] + .into(), + culprit: Default::default(), + transaction: Default::default(), + message: Default::default(), + logentry: Default::default(), + logger: Default::default(), + modules: Default::default(), + server_name: Default::default(), + release: Default::default(), + dist: Default::default(), + environment: Default::default(), + user: Default::default(), + request: Default::default(), + contexts: Default::default(), + breadcrumbs: Default::default(), + stacktrace: Default::default(), + template: Default::default(), + threads: Default::default(), + tags: Default::default(), + extra: Default::default(), + debug_meta: Default::default(), + sdk: Default::default(), + }; + + let mut envelope = Envelope::new(); + envelope.add_item(event); + transport.send_envelope(envelope); + next(info); + })); + }); + + Some(transport) +} + +/// Wrap an async future with [`futures_util::FutureExt::catch_unwind`], +/// capturing any panic as a Sentry exception event and flushing all +/// buffered envelopes before re-raising the panic. +/// +/// Use this in both Durable Object `fetch` / `alarm` handlers and +/// frontend worker `main` functions so that panics are reported to +/// Sentry before the WASM isolate is torn down. +/// +/// The panic hook (installed by [`init`]) captures the event; this +/// function ensures the transport is flushed before the isolate dies. +/// +/// # Example +/// +/// ```ignore +/// async fn fetch(&self, req: Request) -> Result { +/// catch_unwind_and_flush(get_sentry_transport(), self.0.fetch(req)).await +/// } +/// ``` +pub async fn catch_unwind_and_flush(future: F) -> T +where + F: std::future::Future, +{ + use futures_util::FutureExt; + use std::panic::AssertUnwindSafe; + + if PANIC_HOOK_TRANSPORT.get().is_some() { + match AssertUnwindSafe(future).catch_unwind().await { + Ok(val) => val, + Err(payload) => { + // The panic hook already captured the event — we just + // need to flush before re-panicking. + flush().await; + std::panic::resume_unwind(payload); + } + } + } else { + future.await + } +} + +/// Like [`catch_unwind_and_flush`], but also reports `Err` results to +/// Sentry before returning them. +/// +/// This is the standard wrapper for Durable Object `fetch` / `alarm` +/// handlers: it catches panics, and on `Err` it captures the error as +/// a Sentry message with the given tags and flushes. +/// +/// # Errors +/// +/// Returns the inner future's `Err` unchanged after reporting it. +/// +/// # Example +/// +/// ```ignore +/// async fn fetch(&self, req: Request) -> Result { +/// catch_unwind_report_and_flush( +/// get_sentry_transport(), +/// &[("handler", "do_fetch"), ("do_type", "batcher")], +/// self.0.fetch(req), +/// ).await +/// } +/// ``` +pub async fn catch_unwind_report_and_flush( + tags: &[(&str, &str)], + future: F, +) -> Result +where + F: std::future::Future>, + E: std::fmt::Display, +{ + let result = catch_unwind_and_flush(future).await; + if let Err(e) = &result { + capture_error_and_flush(e, sentry_core::Level::Fatal, tags).await; + } + result +} + +/// Capture an error and immediately flush all buffered envelopes. +/// +/// Tags are scoped to this single event via [`sentry_core::with_scope`] +/// so they do not leak into subsequent events on the same hub (which +/// would happen with `configure_scope` in long-lived isolates like DOs). +pub async fn capture_error_and_flush( + error: &dyn std::fmt::Display, + level: sentry_core::Level, + tags: &[(&str, &str)], +) { + sentry_core::with_scope( + |scope| { + for &(key, value) in tags { + scope.set_tag(key, value); + } + }, + || { + sentry_core::capture_message(&error.to_string(), level); + }, + ); + flush().await; +} + +/// Synchronously POST all buffered envelopes to the Sentry ingest endpoint. +/// +/// Call this only on catastrophic errors -- it blocks until all envelopes are +/// sent (or fail). Errors during sending are logged to the console but not +/// propagated. +pub async fn flush() { + let Some(transport) = PANIC_HOOK_TRANSPORT.get() else { + return; + }; + let envelopes = { + let Ok(mut guard) = transport.envelopes.lock() else { + return; + }; + std::mem::take(&mut *guard) + }; + if envelopes.is_empty() { + return; + } + + let dsn = &transport.dsn; + let url = format!( + "{}://{}:{}/api/{}/envelope/", + dsn.scheme(), + dsn.host(), + dsn.port(), + dsn.project_id(), + ); + let auth = format!( + "Sentry sentry_key={}, sentry_version=7, sentry_client=azul/{}", + dsn.public_key(), + env!("CARGO_PKG_VERSION"), + ); + + for envelope in envelopes { + let mut body = Vec::new(); + if let Err(e) = envelope.to_writer(&mut body) { + worker::console_error!("sentry: failed to serialize envelope: {e:?}"); + continue; + } + let req = match worker::Request::new_with_init( + &url, + &worker::RequestInit { + method: worker::Method::Post, + body: Some(body.into()), + ..Default::default() + }, + ) { + Ok(mut r) => { + if let Ok(h) = r.headers_mut() { + let _ = h.set("Content-Type", "application/x-sentry-envelope"); + let _ = h.set("X-Sentry-Auth", &auth); + } + r + } + Err(e) => { + worker::console_error!("sentry: failed to build request: {e:?}"); + continue; + } + }; + if let Err(e) = worker::Fetch::Request(req).send().await { + worker::console_error!("sentry: failed to send envelope: {e:?}"); + } + } +} diff --git a/crates/witness_worker/Cargo.toml b/crates/witness_worker/Cargo.toml index aeaec65..8031b81 100644 --- a/crates/witness_worker/Cargo.toml +++ b/crates/witness_worker/Cargo.toml @@ -34,6 +34,7 @@ console_log.workspace = true ed25519-dalek.workspace = true generic_log_worker.workspace = true getrandom.workspace = true +getrandom_03.workspace = true hex.workspace = true log.workspace = true ml-dsa.workspace = true diff --git a/crates/witness_worker/src/frontend_worker.rs b/crates/witness_worker/src/frontend_worker.rs index ae4d161..683e9cc 100644 --- a/crates/witness_worker/src/frontend_worker.rs +++ b/crates/witness_worker/src/frontend_worker.rs @@ -77,20 +77,25 @@ async fn fetch( env: Env, ctx: Context, ) -> Result> { + crate::init_sentry(&env); let wshim = Wshim::from_env(&env); let registry = metrics::registry(); - let response = Router::new() - .route("/add-checkpoint", post(add_checkpoint)) - .route("/sign-subtree", post(sign_subtree)) - .route("/metadata", get(metadata)) - .route("/", get(root)) - .layer(middleware::from_fn_with_state( - (env.clone(), metrics::FrontendWorkerMetrics::new(®istry)), - request_metrics, - )) - .with_state(env) - .call(req) - .await?; + let response = generic_log_worker::obs::sentry::catch_unwind_and_flush(async { + Router::new() + .route("/add-checkpoint", post(add_checkpoint)) + .route("/sign-subtree", post(sign_subtree)) + .route("/metadata", get(metadata)) + .route("/", get(root)) + .layer(middleware::from_fn_with_state( + (env.clone(), metrics::FrontendWorkerMetrics::new(®istry)), + request_metrics, + )) + .with_state(env) + .call(req) + .await + }) + .await?; + generic_log_worker::obs::sentry::flush().await; if let Ok(wshim) = wshim { ctx.wait_until(async move { wshim.flush(&generic_log_worker::obs::logs::LOGGER).await; diff --git a/crates/witness_worker/src/lib.rs b/crates/witness_worker/src/lib.rs index 80482c3..ea645f4 100644 --- a/crates/witness_worker/src/lib.rs +++ b/crates/witness_worker/src/lib.rs @@ -207,6 +207,16 @@ impl WitnessSigner { /// requests reuse the parsed key. static WITNESS_SIGNER: OnceLock = OnceLock::new(); +/// Initialize sentry from the `SENTRY_DSN` environment variable. +/// +/// Does nothing when the variable is absent or empty, allowing deployments +/// without sentry support. +pub(crate) fn init_sentry(env: &Env) { + if let Ok(dsn) = env.var("SENTRY_DSN") { + let _ = generic_log_worker::obs::sentry::init(&dsn.to_string(), env!("DEPLOY_ENV")); + } +} + /// Load (or return the already-cached) witness signer. /// /// The signing algorithm is derived from the OID in the diff --git a/crates/witness_worker/src/witness_state_do.rs b/crates/witness_worker/src/witness_state_do.rs index 974f353..a7ab681 100644 --- a/crates/witness_worker/src/witness_state_do.rs +++ b/crates/witness_worker/src/witness_state_do.rs @@ -86,12 +86,27 @@ struct WitnessState { state: State, } +// SAFETY: Durable Objects are single-threaded; the `RefUnwindSafe` bound +// is required by `wasm-bindgen` when building with `panic=unwind`. +impl std::panic::RefUnwindSafe for WitnessState {} + impl DurableObject for WitnessState { - fn new(state: State, _env: Env) -> Self { + fn new(state: State, env: Env) -> Self { + crate::init_sentry(&env); Self { state } } - async fn fetch(&self, mut req: Request) -> Result { + async fn fetch(&self, req: Request) -> Result { + generic_log_worker::obs::sentry::catch_unwind_report_and_flush( + &[("handler", "do_fetch"), ("do_type", "witness_state")], + self.fetch_inner(req), + ) + .await + } +} + +impl WitnessState { + async fn fetch_inner(&self, mut req: Request) -> Result { let path = req.path(); match (req.method(), path.as_str()) { (Method::Post, "/check-and-update") => { diff --git a/crates/witness_worker/wrangler.jsonc b/crates/witness_worker/wrangler.jsonc index 1aef12a..e2d6a29 100644 --- a/crates/witness_worker/wrangler.jsonc +++ b/crates/witness_worker/wrangler.jsonc @@ -12,7 +12,10 @@ // DEPLOY_ENV is used in build.rs to select the per-environment // config. Change '--release' to '--dev' to compile with debug // symbols. - "command": "cargo install -q worker-build@0.7.5 && DEPLOY_ENV=dev worker-build --release" + // RUSTFLAGS: force legacy Wasm EH so wasm-bindgen 0.2.126's + // wasmparser accepts the module (modern exnref opcodes from + // nightly ≥2026-05-09 are not yet supported). + "command": "cargo install -q worker-build@0.8.5 && DEPLOY_ENV=dev RUSTFLAGS='-Cllvm-args=-wasm-use-legacy-eh' worker-build --release --panic-unwind" }, "workers_dev": true, "durable_objects": {