{"record":{"id":"350f766669b52258","repo":"zeroclaw-labs/zeroclaw","slug":"serial-path-not-allowed-allowed","errorCode":null,"errorMessage":"Serial path not allowed: {}. Allowed: {}","messagePattern":"Serial path not allowed: (.+?)\\. Allowed: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-hardware/src/peripherals/serial.rs","lineNumber":148,"sourceCode":"}\n\nimpl SerialPeripheral {\n    /// Create and connect to a serial peripheral.\n    #[allow(clippy::unused_async)]\n    pub async fn connect(config: &PeripheralBoardConfig) -> anyhow::Result<Self> {\n        let path = config.path.as_deref().ok_or_else(|| {\n            ::zeroclaw_log::record!(\n                WARN,\n                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)\n                    .with_outcome(::zeroclaw_log::EventOutcome::Failure)\n                    .with_attrs(::serde_json::json!({\"board\": config.board})),\n                \"serial peripheral connect refused: config missing 'path'\"\n            );\n            anyhow::Error::msg(\"Serial peripheral requires path\")\n        })?;\n\n        if !is_serial_path_allowed(path) {\n            anyhow::bail!(\n                \"Serial path not allowed: {}. Allowed: {}\",\n                path,\n                serial_path_allowlist_hint()\n            );\n        }\n\n        let builder = tokio_serial::new(path, serial_open_baud(path, config.baud));\n        #[cfg(unix)]\n        let builder = if should_open_serial_nonexclusive(path) {\n            builder.exclusive(false)\n        } else {\n            builder\n        };\n        let port = builder.open_native_async().map_err(|e| {\n            ::zeroclaw_log::record!(\n                ERROR,\n                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)\n                    .with_outcome(::zeroclaw_log::EventOutcome::Failure)","sourceCodeStart":130,"sourceCodeEnd":166,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-hardware/src/peripherals/serial.rs#L130-L166","documentation":"SerialPeripheral::connect enforces a hardcoded device-path allowlist: /dev/ttyACM*, /dev/ttyUSB*, /dev/tty.usbmodem*, /dev/cu.usbmodem*, /dev/tty.usbserial*, /dev/cu.usbserial*, COM* (plus /tmp/zc-sim-* when built with the dev-sim feature). Any configured path that does not start with one of these prefixes is rejected before the port is opened; the message lists the accepted prefixes.","triggerScenarios":"Configuring a [[peripherals.boards]] entry with path = \"/dev/ttyS0\" (legacy UART), a stable symlink like /dev/serial/by-id/usb-... (fails: not an allowed prefix), a relative path like ttyACM0, or a typo such as /dev/ttyACN0.","commonSituations":"Boards exposing legacy ttyS* nodes; users preferring /dev/serial/by-id paths for stability; lowercase com3 on Windows; configs ported from tools that accept any device path.","solutions":["Point path at the device's real node under an allowed prefix: /dev/ttyACM* or /dev/ttyUSB* on Linux, /dev/cu.usbmodem* on macOS, COM<n> on Windows","Do not use /dev/serial/by-id or /dev/ttyS* paths — the allowlist matches prefixes literally; use the underlying ttyACM/ttyUSB node instead","Fix format details: uppercase COM with a number; the leading /dev/ is required on Unix"],"exampleFix":"# before\n[[peripherals.boards]]\nboard = \"nucleo-f401re\"\ntransport = \"serial\"\npath = \"/dev/serial/by-id/usb-STMicroelectronics-CDC-1234\"\n\n# after\n[[peripherals.boards]]\nboard = \"nucleo-f401re\"\ntransport = \"serial\"\npath = \"/dev/ttyACM0\"","handlingStrategy":"validation","validationCode":"const ALLOWED: [&str; 7] = [\n    \"/dev/ttyACM\", \"/dev/ttyUSB\", \"/dev/tty.usbmodem\", \"/dev/cu.usbmodem\",\n    \"/dev/tty.usbserial\", \"/dev/cu.usbserial\", \"COM\",\n];\nfn serial_path_allowed(path: &str) -> bool {\n    ALLOWED.iter().any(|p| path.starts_with(p))\n}\n\nif !serial_path_allowed(&config.path.clone().unwrap_or_default()) {\n    anyhow::bail!(\"serial path rejected by allowlist; use /dev/ttyACM* or COM*\");\n}\nSerialPeripheral::connect(&config).await?;","typeGuard":"fn is_allowed_serial_path(path: &str) -> bool {\n    [\"/dev/ttyACM\", \"/dev/ttyUSB\", \"/dev/tty.usbmodem\", \"/dev/cu.usbmodem\",\n     \"/dev/tty.usbserial\", \"/dev/cu.usbserial\", \"COM\"]\n        .iter().any(|p| path.starts_with(p))\n}","tryCatchPattern":null,"preventionTips":["Always configure the real ttyACM/ttyUSB/usbmodem/COM node, not symlinks like /dev/serial/by-id","Validate the path against the prefix list in config-loading code, not at connect time","Remember the allowlist is hardcoded — /dev/ttyS* devices cannot be used"],"tags":["serial","allowlist","device-path","config","security"],"backgroundTag":"path-allowlist-rejected","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}