| CRITICAL |
crates/fbuild-build/src/zccache_embedded.rs:215-217 |
runtime.block_on(async move { inner.compile(req).await }) inside FbuildZccacheService::compile_blocking |
Every per-TU compile (called from compiler.rs:665 via compile_source) blocks on this with no tokio::time::timeout. Hangs in the zccache backend stall a daemon HTTP worker until the user kills the process. |
Wrap with tokio::time::timeout(Duration::from_secs(300), inner.compile(req)), return EmbeddedServiceError::Timeout, surface as FbuildError::Timeout at the compiler.rs:665 call site. 5 min is a reasonable upper bound for any single compile. |
| CRITICAL |
crates/fbuild-build/src/zccache_embedded.rs:150-152 |
ZccacheService::start(cfg).await in start_in |
Called once from the daemon's #[tokio::main] via CompileBackend::start (compile_backend.rs:48) before any HTTP listener comes up. If start hangs (e.g. corrupt cache_root, identity hash livelock) the daemon never finishes startup and never logs a useful error. Affects 100% of users. |
Wrap with tokio::time::timeout(Duration::from_secs(30), ZccacheService::start(cfg)). |
| CRITICAL |
crates/fbuild-build/src/zccache_embedded.rs:227-230 |
FbuildZccacheService::flush().await |
Called from shutdown path and (via FbuildZccacheService) potentially from daemon shutdown hooks. Unbounded flush blocks daemon shutdown. |
Wrap with tokio::time::timeout(Duration::from_secs(15), self.inner.flush()). |
| CRITICAL |
crates/fbuild-build/src/zccache_embedded.rs:250-256 |
ZccacheService::shutdown(mode).await |
Same risk as flush, plus inability to terminate the daemon cleanly. |
Wrap with tokio::time::timeout(Duration::from_secs(30), svc.shutdown(mode)). |
| HIGH |
crates/fbuild-build/src/script_runtime.rs:111 |
run_command(&argv, Some(project_dir), None, None) running user extra_scripts Python harness |
User-supplied Python code; a buggy/malicious extra_scripts entry hangs the build. Hot path for any project using extra_scripts in platformio.ini. |
Pass Some(Duration::from_secs(60)) — extra_scripts is config-time evaluation, never legitimately long. |
| HIGH |
crates/fbuild-build/src/script_runtime.rs:297 |
run_command(&argv, None, None, None) running python --version for python discovery |
Trivial command but on the startup path. A hung Python interpreter wedges build init. |
Some(Duration::from_secs(5)). |
| HIGH |
crates/fbuild-build/src/compiler.rs:438 |
run_command(&[gcc, "-dumpversion"], None, None, None) in compiler_version |
Trivial command, but cached per compiler path and called on every fresh build. A wedged toolchain binary (corrupt EXE, missing DLL hang on Windows) blocks the whole pipeline. |
Some(Duration::from_secs(5)). |
| HIGH |
crates/fbuild-build/src/pipeline/compile.rs:167-172 |
run_command(&[gcc, "-dumpversion"], ...) in log_toolchain_version |
Same risk as compiler_version — gcc -dumpversion should never block. |
Some(Duration::from_secs(5)). |
| HIGH |
crates/fbuild-build/src/linker.rs:331 |
run_command(&args_ref, None, None, None) — ar rcs <archive> <objects...> in LinkerBase::archive |
Used by every platform (avr, esp32, esp8266, nrf52, sam, silabs, teensy, etc.) to build the framework core archive. ar can occasionally hang on very large arg lists on Windows. |
Some(Duration::from_secs(120)). |
| HIGH |
crates/fbuild-build/src/linker.rs:359 |
run_command(...) — <prefix>-size firmware.elf in LinkerBase::report_size |
Final-stage size report on the build. |
Some(Duration::from_secs(15)). |
| HIGH |
crates/fbuild-build/src/linker.rs:409 |
run_command(...) — <prefix>-nm --print-size --size-sort ... in LinkerBase::analyze_symbols |
Symbol map analysis at end of build. |
Some(Duration::from_secs(30)). |
| HIGH |
crates/fbuild-build/src/linker.rs:455 |
run_command(...) — <prefix>-objcopy -O <fmt> <elf> <hex> in LinkerBase::objcopy_firmware |
ELF to HEX/BIN conversion used by AVR and Teensy. |
Some(Duration::from_secs(60)). |
| HIGH |
crates/fbuild-build/src/avr/avr_linker.rs:187 |
run_command(...) — main avr-gcc link step |
Hot path: the primary link command for every AVR build. |
Some(Duration::from_secs(180)). |
| HIGH |
crates/fbuild-build/src/ch32v/ch32v_linker.rs:113 |
run_command(...) — main riscv-none-elf-gcc link step |
Hot path for CH32V builds. |
Some(Duration::from_secs(180)). |
| HIGH |
crates/fbuild-build/src/esp32/esp32_linker.rs:325 |
run_command(...) — main ESP32 link (Windows response-file path) |
Hot path for ESP32 link on Windows. Already has a 30s sibling at line 381 for elf2image — the link itself should also have one. |
Some(Duration::from_secs(300)) — ESP32 links can be long. |
| HIGH |
crates/fbuild-build/src/esp32/esp32_linker.rs:328 |
run_command(...) — main ESP32 link (non-Windows path) |
Same as above. |
Some(Duration::from_secs(300)). |
| HIGH |
crates/fbuild-build/src/esp32/orchestrator/embed.rs:72 |
run_command(...) — objcopy for board_build.embed_files binary embedding |
Per-file in user's embed_files list. |
Some(Duration::from_secs(30)). |
| HIGH |
crates/fbuild-build/src/esp32/orchestrator/embed.rs:132 |
run_command(...) — objcopy for board_build.embed_txtfiles |
Same as above for text embedding. |
Some(Duration::from_secs(30)). |
| HIGH |
crates/fbuild-build/src/esp8266/esp8266_linker.rs:121 |
run_command(...) — gcc -E -P linker-script preprocessing |
Build-time preprocessor invocation. |
Some(Duration::from_secs(30)). |
| HIGH |
crates/fbuild-build/src/esp8266/esp8266_linker.rs:228 |
run_command(...) — main xtensa-lx106-elf-gcc link step |
Hot path for ESP8266 builds. |
Some(Duration::from_secs(180)). |
| HIGH |
crates/fbuild-build/src/nrf52/nrf52_linker.rs:128 |
run_command(...) — main link step |
Hot path for nRF52 builds. |
Some(Duration::from_secs(180)). |
| HIGH |
crates/fbuild-build/src/sam/sam_linker.rs:149 |
run_command(...) — main link step |
Hot path for SAM/Due builds. |
Some(Duration::from_secs(180)). |
| HIGH |
crates/fbuild-build/src/renesas/renesas_linker.rs:133 |
run_command(...) — main link step |
Hot path for Renesas (UNO R4) builds. |
Some(Duration::from_secs(180)). |
| HIGH |
crates/fbuild-build/src/generic_arm/arm_linker.rs:173 |
run_command(...) — main link (Windows response-file path) |
Hot path for generic ARM builds. |
Some(Duration::from_secs(180)). |
| HIGH |
crates/fbuild-build/src/generic_arm/arm_linker.rs:176 |
run_command(...) — main link (non-Windows path) |
Same as above. |
Some(Duration::from_secs(180)). |
| HIGH |
crates/fbuild-build/src/teensy/teensy_linker.rs:166 |
run_command(...) — main link (Windows response-file path) |
Hot path for Teensy. teensy41 has ~500 .o files. |
Some(Duration::from_secs(180)). |
| HIGH |
crates/fbuild-build/src/teensy/teensy_linker.rs:169 |
run_command(...) — main link (non-Windows path) |
Same as above. |
Some(Duration::from_secs(180)). |
| HIGH |
crates/fbuild-build/src/silabs/silabs_linker.rs:136 |
run_command(...) — main link step |
Hot path for SiLabs builds. |
Some(Duration::from_secs(180)). |
| HIGH |
crates/fbuild-build/src/stm32/orchestrator/arduino_mbed.rs:272 |
run_command(...) — g++ -E -P linker-script preprocessing for STM32 Arduino-mbed variant |
Build-time preprocessor invocation. |
Some(Duration::from_secs(30)). |
| MEDIUM |
crates/fbuild-build/src/symbol_analyzer/mod.rs:178 |
run_command_with_stdin(&[cppfilt], stdin, ..., None) in demangle_batch |
End-of-build bloat analysis. c++filt is generally well-behaved but the stdin payload can be hundreds of KB; a wedged child would freeze the symbol report. |
Some(Duration::from_secs(60)). |
| MEDIUM |
crates/fbuild-build/src/symbol_analyzer/mod.rs:228 |
run_command(&[nm, "--print-size", ...], None, None, None) in analyze_elf |
End-of-build symbol analysis. |
Some(Duration::from_secs(60)). |
| MEDIUM |
crates/fbuild-build/src/symbol_analyzer/mod.rs:370 |
run_command(&[objdump, "-d", "--no-show-raw-insn", elf], ..., None) for callgraph extraction |
End-of-build, off the critical path but still daemon-thread. objdump -d on a large ESP32 ELF can produce 100+ MB of output. |
Some(Duration::from_secs(120)). |
Intro
Audited
crates/fbuild-build/for blocking operations that have notimeout / deadline / cancellation. Scope: build orchestration, per-platform
orchestrators (AVR, ESP32, ESP8266, RP2040, STM32, Teensy, SAM, NRF52, CH32V,
Renesas, Silabs, NXP-LPC, Apollo3, generic-arm), the shared compile pipeline,
linker invocations, the embedded
zccachecompile dispatch, theextra_scriptsPython harness, and the symbol/bloat analyzers (
nm,objdump,c++filt,size,objcopy,ar).The dominant failure mode in this crate is subprocess invocations of toolchain
binaries via
fbuild_core::subprocess::run_command(..., timeout: None).That helper does support a
timeout: Option<Duration>(withTimeouterrormapping and
process.kill()on expiry — seecrates/fbuild-core/src/subprocess.rslines 81-208), so the fix at every site below is mechanical: pass
Some(Duration::from_secs(N))with an N chosen to fit the operation(5-30s for utility tools like
nm/size/objcopy/-dumpversion, longer foractual link steps that legitimately run for minutes on large ESP32 firmware).
A handful of utility sites already do this correctly (e.g.
esp32_linker.rs:381elf2imagewith 30s,boot_artifacts.rs:94/145,shrink/probe.rs:352,esp8266_linker.rs:262) — they should be the template for the missing ones.The second class of finding is the embedded zccache dispatch in
zccache_embedded.rs/compile_backend.rs. Every per-translation-unit compileflows through
FbuildZccacheService::compile_blocking, which callstokio::runtime::Handle::block_on(svc.compile(req).await)with notokio::time::timeout(...)wrapper. If the upstreamZccacheService::compileever hangs (network-mode cache backends, deadlocked fingerprint scan, etc.)
the build worker thread parks forever. This is the single highest-impact
finding because it is on the hot compile path inside the daemon.
The third class is the
extra_scriptsPython harness invocation inscript_runtime.rs. User-suppliedextra_scripts = [...]inplatformio.iniare evaluated by spawning Python with no timeout — a hostile or buggy script
can wedge the daemon's build pipeline indefinitely.
Findings
crates/fbuild-build/src/zccache_embedded.rs:215-217runtime.block_on(async move { inner.compile(req).await })insideFbuildZccacheService::compile_blockingcompiler.rs:665viacompile_source) blocks on this with notokio::time::timeout. Hangs in the zccache backend stall a daemon HTTP worker until the user kills the process.tokio::time::timeout(Duration::from_secs(300), inner.compile(req)), returnEmbeddedServiceError::Timeout, surface asFbuildError::Timeoutat thecompiler.rs:665call site. 5 min is a reasonable upper bound for any single compile.crates/fbuild-build/src/zccache_embedded.rs:150-152ZccacheService::start(cfg).awaitinstart_in#[tokio::main]viaCompileBackend::start(compile_backend.rs:48) before any HTTP listener comes up. Ifstarthangs (e.g. corrupt cache_root, identity hash livelock) the daemon never finishes startup and never logs a useful error. Affects 100% of users.tokio::time::timeout(Duration::from_secs(30), ZccacheService::start(cfg)).crates/fbuild-build/src/zccache_embedded.rs:227-230FbuildZccacheService::flush().awaitshutdownpath and (viaFbuildZccacheService) potentially from daemon shutdown hooks. Unboundedflushblocks daemon shutdown.tokio::time::timeout(Duration::from_secs(15), self.inner.flush()).crates/fbuild-build/src/zccache_embedded.rs:250-256ZccacheService::shutdown(mode).awaitflush, plus inability to terminate the daemon cleanly.tokio::time::timeout(Duration::from_secs(30), svc.shutdown(mode)).crates/fbuild-build/src/script_runtime.rs:111run_command(&argv, Some(project_dir), None, None)running userextra_scriptsPython harnessextra_scriptsentry hangs the build. Hot path for any project usingextra_scriptsinplatformio.ini.Some(Duration::from_secs(60))— extra_scripts is config-time evaluation, never legitimately long.crates/fbuild-build/src/script_runtime.rs:297run_command(&argv, None, None, None)runningpython --versionfor python discoverySome(Duration::from_secs(5)).crates/fbuild-build/src/compiler.rs:438run_command(&[gcc, "-dumpversion"], None, None, None)incompiler_versionSome(Duration::from_secs(5)).crates/fbuild-build/src/pipeline/compile.rs:167-172run_command(&[gcc, "-dumpversion"], ...)inlog_toolchain_versioncompiler_version—gcc -dumpversionshould never block.Some(Duration::from_secs(5)).crates/fbuild-build/src/linker.rs:331run_command(&args_ref, None, None, None)—ar rcs <archive> <objects...>inLinkerBase::archivearcan occasionally hang on very large arg lists on Windows.Some(Duration::from_secs(120)).crates/fbuild-build/src/linker.rs:359run_command(...)—<prefix>-size firmware.elfinLinkerBase::report_sizeSome(Duration::from_secs(15)).crates/fbuild-build/src/linker.rs:409run_command(...)—<prefix>-nm --print-size --size-sort ...inLinkerBase::analyze_symbolsSome(Duration::from_secs(30)).crates/fbuild-build/src/linker.rs:455run_command(...)—<prefix>-objcopy -O <fmt> <elf> <hex>inLinkerBase::objcopy_firmwareSome(Duration::from_secs(60)).crates/fbuild-build/src/avr/avr_linker.rs:187run_command(...)— mainavr-gcclink stepSome(Duration::from_secs(180)).crates/fbuild-build/src/ch32v/ch32v_linker.rs:113run_command(...)— mainriscv-none-elf-gcclink stepSome(Duration::from_secs(180)).crates/fbuild-build/src/esp32/esp32_linker.rs:325run_command(...)— main ESP32 link (Windows response-file path)elf2image— the link itself should also have one.Some(Duration::from_secs(300))— ESP32 links can be long.crates/fbuild-build/src/esp32/esp32_linker.rs:328run_command(...)— main ESP32 link (non-Windows path)Some(Duration::from_secs(300)).crates/fbuild-build/src/esp32/orchestrator/embed.rs:72run_command(...)—objcopyforboard_build.embed_filesbinary embeddingembed_fileslist.Some(Duration::from_secs(30)).crates/fbuild-build/src/esp32/orchestrator/embed.rs:132run_command(...)—objcopyforboard_build.embed_txtfilesSome(Duration::from_secs(30)).crates/fbuild-build/src/esp8266/esp8266_linker.rs:121run_command(...)—gcc -E -Plinker-script preprocessingSome(Duration::from_secs(30)).crates/fbuild-build/src/esp8266/esp8266_linker.rs:228run_command(...)— mainxtensa-lx106-elf-gcclink stepSome(Duration::from_secs(180)).crates/fbuild-build/src/nrf52/nrf52_linker.rs:128run_command(...)— main link stepSome(Duration::from_secs(180)).crates/fbuild-build/src/sam/sam_linker.rs:149run_command(...)— main link stepSome(Duration::from_secs(180)).crates/fbuild-build/src/renesas/renesas_linker.rs:133run_command(...)— main link stepSome(Duration::from_secs(180)).crates/fbuild-build/src/generic_arm/arm_linker.rs:173run_command(...)— main link (Windows response-file path)Some(Duration::from_secs(180)).crates/fbuild-build/src/generic_arm/arm_linker.rs:176run_command(...)— main link (non-Windows path)Some(Duration::from_secs(180)).crates/fbuild-build/src/teensy/teensy_linker.rs:166run_command(...)— main link (Windows response-file path).ofiles.Some(Duration::from_secs(180)).crates/fbuild-build/src/teensy/teensy_linker.rs:169run_command(...)— main link (non-Windows path)Some(Duration::from_secs(180)).crates/fbuild-build/src/silabs/silabs_linker.rs:136run_command(...)— main link stepSome(Duration::from_secs(180)).crates/fbuild-build/src/stm32/orchestrator/arduino_mbed.rs:272run_command(...)—g++ -E -Plinker-script preprocessing for STM32 Arduino-mbed variantSome(Duration::from_secs(30)).crates/fbuild-build/src/symbol_analyzer/mod.rs:178run_command_with_stdin(&[cppfilt], stdin, ..., None)indemangle_batchc++filtis generally well-behaved but the stdin payload can be hundreds of KB; a wedged child would freeze the symbol report.Some(Duration::from_secs(60)).crates/fbuild-build/src/symbol_analyzer/mod.rs:228run_command(&[nm, "--print-size", ...], None, None, None)inanalyze_elfSome(Duration::from_secs(60)).crates/fbuild-build/src/symbol_analyzer/mod.rs:370run_command(&[objdump, "-d", "--no-show-raw-insn", elf], ..., None)for callgraph extractionobjdump -don a large ESP32 ELF can produce 100+ MB of output.Some(Duration::from_secs(120)).What was searched
Patterns run via Grep over
crates/fbuild-build/src/:Command::new— no production matches (only test files incrates/fbuild-build/tests/)\.output\(\)— only tests\.wait\(\)|\.wait_with_output\(\)|wait_timeout— onlyBarrier::wait()in testsreqwest|TcpStream|TcpListener— no matches insrc/(onlyCargo.tomldep)tokio::time::timeout|tokio::time::sleep|tokio::process— no matches in this crate\.await— only incompile_backend.rsandzccache_embedded.rs(audited individually above)\.lock\(\)— all arestd::sync::Mutex::lock(), scoped to local data structures (work_iter,first_error,results_slot,build_log,COMPILER_IDENTITY_CACHE, fingerprint cacheentries), not external state. No deadlock risk identified — locks are held only across small map/insert/push ops, never acrossawaitpoints or subprocess waits.\.read\(\)\.unwrap|\.write\(\)\.unwrap— no matches (noRwLockuse)\.recv\(\)|recv_timeout|mpsc::— onlympsc::Senderinbuild_output.rsandlib.rs(no receivers in this crate; receiver lives infbuild-daemon's websocket pump and is out of scope)thread::sleep|Instant::now|Duration::from—Instant::nowis everywhere for perf timing (not for polling).thread::sleeponly in test code.block_on— single production occurrence inzccache_embedded.rs:216(covered above).run_command\(/run_command_with_stdin/run_command_passthrough— every call site catalogued. Sites withSome(Duration::...)already (good template):esp32_linker.rs:381— 30s foresptool elf2imageesp8266_linker.rs:262— 30s foresptool elf2imageesp32/orchestrator/boot_artifacts.rs:94— 30s for bootloaderelf2imageesp32/orchestrator/boot_artifacts.rs:145— 10s forgen_esp32part.pyshrink/probe.rs:352— 5s for probe compileFiles spot-checked for full context:
crates/fbuild-core/src/subprocess.rs— confirmedrun_commandhonors timeout viaprocess.wait(timeout)and kills the child onProcessError::Timeout.crates/fbuild-build/src/compiler.rs— confirmedcompile_sourceis the single funnel from every platform compiler module intocompile_blocking.crates/fbuild-build/src/parallel.rs— confirmed theloop {}in the worker terminates when thework_iterruns out; not a busy-poll.crates/fbuild-build/src/build_fingerprint/fast_path.rs—Mutexuse is bounded to local map operations.Out-of-scope notes
crates/fbuild-build/tests/*.rsand*_tests.rsmodules — test code withCommand::new,Barrier::wait(), andthread::sleepfor synchronization. These can hang CI if they break but the fix is per-test (most already use bounded sleeps under 50ms).Dropimpls — noDropimpls in this crate perform blocking IO. (The embeddedZccacheServiceDropis upstream inzccacheand out of scope per audit: blocking operations with no timeout (meta) #802's audit guidance.)running_processcrate internals — the subprocess primitive itself has timeout support; bugs in its implementation are out of scope for this audit.fbuild-daemonHTTP handlers — out of scope (separate auditor area).compile_database/— pure JSON serialization; no blocking IO besidesstd::fswhich is filesystem-bounded.eh_frame_policy*.rs,flag_overlay.rs,arduino_props.rs,package_override.rs,framework_libs.rs,framework_core_cache.rs,resolution.rs,lib.rs— pure config / data transformation, no subprocess or locking concerns.source_scanner/— usesio::stderr().lock()for line-by-line output flushing; not unbounded blocking.Sub-issue of #802.