diff --git a/Makefile b/Makefile index 4b92cf33..697df0ef 100644 --- a/Makefile +++ b/Makefile @@ -834,10 +834,7 @@ bench-gate: @printf "$(GREEN)Checking connector allocation counts...$(NC)\n" cargo bench --package aimdb-bench --bench b0_alloc_connector -## Drift guards -# The README quickstart is compiled as examples/readme-quickstart; this target -# fails when the README code block and the example diverge, or when the -# example no longer compiles (design 038 §2.6/§3.13). +## Interop # ros2:// against a real ROS 2 distribution (rmw_zenohd and the ros2 CLI in # Docker, host networking). DISTRO is a ros: image tag. DISTRO ?= lyrical @@ -846,6 +843,10 @@ ros2-interop: cargo build --package aimdb-zenoh-connector --features "std" --examples ./aimdb-zenoh-connector/tests/interop/ros2_interop.sh $(DISTRO) +## Drift guards +# The README quickstart is compiled as examples/readme-quickstart; this target +# fails when the README code block and the example diverge, or when the +# example no longer compiles (design 038 §2.6/§3.13). readme-check: @printf "$(GREEN)Checking README quickstart against examples/readme-quickstart...$(NC)\n" @awk '/^```rust$$/{f=1;next} f&&/^```$$/{exit} f' README.md \ diff --git a/aimdb-zenoh-connector/README.md b/aimdb-zenoh-connector/README.md index 62ca0ce0..1f4c2950 100644 --- a/aimdb-zenoh-connector/README.md +++ b/aimdb-zenoh-connector/README.md @@ -137,6 +137,13 @@ builder.configure::("cell4.temperature", |reg| { subscription on the session; one that panics drops its sample, logged. - **Keys are at most 65,535 bytes**, the Zenoh wire format's limit: a longer fixed key fails the build, a longer written key is skipped. +- **An inner `**` is exposed to + [zenoh#2836](https://github.com/eclipse-zenoh/zenoh/issues/2836).** Zenoh + checks each delivered sample against every subscription with `intersects`, + which recurses once per chunk under a `**` that more chunks follow. On an + inbound link such as `zenoh://site/{path..}/state`, one sample with a long + matching key (about 64 KB) overflows a stack and aborts the process. A `**` + or `{name..}` at the end of a link is not affected. - **Transport compression stays off.** zenoh 1.10.1 decompresses through `lz4_flex` 0.10.0, which has RUSTSEC-2026-0041; this crate never enables it. An application whose own dependencies enable `zenoh`'s compression, for diff --git a/docs/aimdb-usage-guide.md b/docs/aimdb-usage-guide.md index e9a37b94..5a894265 100644 --- a/docs/aimdb-usage-guide.md +++ b/docs/aimdb-usage-guide.md @@ -443,15 +443,18 @@ aimdb-data-contracts = { version = "0.2", features = ["ros2"] } # #[derive(Ros ```rust use aimdb_zenoh_connector::{Ros2Connector, Ros2Node, ZenohConnector}; -// Plain Zenoh +// Plain Zenoh, and ROS 2 on the same session: register both let zenoh = ZenohConnector::new("tcp/192.168.10.5:7447"); +let ros2 = zenoh.ros2(Ros2Node::new("cell4_gateway")).register::(); +let mut builder = AimDbBuilder::new() + .runtime(runtime) + .with_connector(zenoh) + .with_connector(ros2); + builder.configure::("cell4.state", |reg| { reg.buffer(BufferCfg::SingleLatest) .linked_to_with("zenoh://aimdb/cell4/state", Postcard::<128>); }); - -// ROS 2, on the same session -let ros2 = zenoh.ros2(Ros2Node::new("cell4_gateway")).register::(); builder.configure::("cell4.temperature", |reg| { reg.buffer(BufferCfg::SpmcRing { capacity: 16 }) .linked_to("ros2://cell4/temperature"); diff --git a/docs/design/053-zenoh-connector.md b/docs/design/053-zenoh-connector.md index 8178cf91..07c74ad5 100644 --- a/docs/design/053-zenoh-connector.md +++ b/docs/design/053-zenoh-connector.md @@ -441,6 +441,13 @@ from a Zenoh or rmw_zenoh config work unchanged. The native backend also accepts subscriber. Subscriber `i`'s callback therefore dispatches only if no earlier subscription matches the key, so each route still sees it once (s08, tested). 055 §5.7's MQTT 5 duplicate does not occur on Zenoh. + - zenoh's `Session` still checks each delivered sample against every + subscriber's key with `intersects`, which recurses once per chunk of the + sample's key under a `**` that chunks follow (zenoh#2836). A filter + such as `aimdb/{path..}/state` is therefore exposed: one long matching + key aborts the process. **[verified]** on zenoh 1.10.1 with a + 64,001-byte key and a client subscribed to `a/**/b` through a router. + A trailing `**` is not affected. - An inbound `with_topic_resolver` is honoured: its result goes through the same grammar. - **`ZenohGrammar`** implements `TopicGrammar` (055 §5.1). `{name}` compiles diff --git a/docs/upstream/zenoh-2836-keyexpr-intersects-stack-overflow.md b/docs/upstream/zenoh-2836-keyexpr-intersects-stack-overflow.md index b91bb843..d7a665b5 100644 --- a/docs/upstream/zenoh-2836-keyexpr-intersects-stack-overflow.md +++ b/docs/upstream/zenoh-2836-keyexpr-intersects-stack-overflow.md @@ -7,11 +7,29 @@ ## Why it matters to AimDB -`ZenohGrammar` does its own matching and does not call `intersects`, so the -connector is not affected (053 §4.3). Its matcher handles a 64 KB key in about -a millisecond (`long_keys_match_in_linear_time`). The issue matters if the -connector ever delegates matching to `zenoh-keyexpr`, and for the grammar -oracle, whose corpus keeps keys short. +`ZenohGrammar` does its own matching and does not call `intersects`; it +handles a 64 KB key in about a millisecond (`long_keys_match_in_linear_time`). +zenoh calls it, though: `Session` matches every sample it delivers against +each subscriber's key expression with `intersects` (`subscriber_callbacks` in +`zenoh/src/api/session.rs`, 1.10.1). So an inbound `zenoh://` link whose +filter has a `**` that other chunks follow, such as +`zenoh://site/{path..}/state`, is reachable from the network: any client that +can publish a matching key through the router can abort the process. + +Measured on zenoh 1.10.1, release build. A client subscribed to `a/**/b` +through a router received one `put` on a matching key of 32,000 chunks +(64,001 bytes, inside the wire format's limit). Its receive thread (`rx-0`) +overflowed its stack and the process aborted; the router survived. Peer to +peer, `**/b` and `a/*/**/b` aborted the same way. A trailing `**` is not +affected: `a/**` received a key of 32,000 chunks, because `intersects` +returns at once when the `**` is last. + +The connector has no guard yet; its README lists this under Limits. A guard +could declare such a filter only up to its first `**` and match the filter +itself in the callback, except a filter with a verbatim chunk after that +`**`, which no wider key covers. The issue also matters if the connector ever +delegates matching to `zenoh-keyexpr`, and for the grammar oracle, whose +corpus keeps keys short. ## Issue as filed