From 4b31cf951862652fc95d807422ffd43e55ee44a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 12:23:30 +0000 Subject: [PATCH] docs(zenoh): document the zenoh#2836 exposure and fix two nits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - zenoh's Session runs `intersects` on every delivered sample against each subscriber's key, so an inbound zenoh:// link whose filter has a `**` before more chunks (`site/{path..}/state`) can be aborted by one sample with a long matching key. Measured on zenoh 1.10.1 through a router: the subscribing client aborted, the router survived. Correct the upstream note, which said the connector was not affected, and add the exposure to design 053 §4.3 and to the connector README's limits. - Makefile: move ros2-interop under its own header, so readme-check keeps its comment. - Usage guide: register both Zenoh connectors in the example. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019466ean3DmpcQPdR1DApoT --- Makefile | 9 +++--- aimdb-zenoh-connector/README.md | 7 +++++ docs/aimdb-usage-guide.md | 11 +++++--- docs/design/053-zenoh-connector.md | 7 +++++ ...-2836-keyexpr-intersects-stack-overflow.md | 28 +++++++++++++++---- 5 files changed, 49 insertions(+), 13 deletions(-) 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