Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Unix cache signals lose their structure argument and are rejected by AT-SPI

Closed
#818 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

A pull request for this has already been merged.

  • #819 by @luccahuguet — merged

Assessment

Difficulty
3/5
Estimated time
Half a day
Newbie friendliness
18/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
linux, rust
Domain
desktop-dev

Research direction

The bug is in the emit_cache_signal helper in adapters/unix/src/atspi/bus.rs, which passes the structure as the message body instead of wrapping it in a one-element tuple. The regression test from the issue, cache_signals_preserve_struct_arguments, needs a private bus, so run it with dbus-run-session -- cargo test --locked -p accesskit_unix ... -- --ignored. Done means the encoded SIGNATURE header matches the expected struct signatures. PR #819 already proposes this fix, so check it before starting.

Written by the indexing model from the issue text.

Description

The Unix adapter emits AddAccessible and RemoveAccessible with flattened D-Bus signatures. Each signal should contain one structure argument, but the shared emit_cache_signal helper passes that structure directly as the message body.

Reproduced on upstream commit 466e24e252103f4d82bd6fbab98141b551e46e0c, using its unchanged lockfile: accesskit_unix 0.24.0, zbus 5.19.0, zvariant 5.15.0, and Rust 1.87.0 on x86_64 Linux.

Signal Expected wire signature Observed wire signature
AddAccessible ((so)(so)(so)iiassusau) (so)(so)(so)iiassusau
RemoveAccessible (so) so

GNOME AT-SPI 2.58.1 rejects the malformed AddAccessible signature; its RemoveAccessible handler likewise requires (so).

To reproduce, save the regression patch below as regression-only.patch and apply it to that upstream commit, then run:

dbus-run-session -- cargo test --locked -p accesskit_unix \
  cache_signals_preserve_struct_arguments -- --ignored

The test calls the production helper on a private bus and checks the encoded D-Bus SIGNATURE header and round-tripped values. It fails with the original helper and passes after the two-line correction below. Inspecting only zbus's parsed signature can hide the missing structure boundary.

The proposed correction in PR #819 is to pass &(body,) to emit_signal, with the generic bound changed from DynamicType to Type so the tuple can be serialized. No dependency or lockfile changes are needed.

We also verified the backported correction in our Venus terminal client on native Wayland with Orca 49.4 and AT-SPI 2.58.1. Previously Orca retained stale focus and reported “No selected text” after returning from scrollback despite a valid selection. With the correction it reported the exact selected text é 界 without a focus-repair action. Those native checks used accesskit_unix 0.22.1, zbus 5.18.0, and zvariant 5.13.1; current upstream was verified at the wire level.

regression-only.patch (test only; production helper unchanged)
diff --git a/adapters/unix/src/atspi/bus.rs b/adapters/unix/src/atspi/bus.rs
index 3368185..ca9857d 100644
--- a/adapters/unix/src/atspi/bus.rs
+++ b/adapters/unix/src/atspi/bus.rs
@@ -523,3 +523,74 @@ where
         Err(_) => Ok(default),
     }
 }
+
+#[cfg(all(test, not(feature = "tokio")))]
+mod tests {
+    use super::*;
+    use atspi::CacheItem;
+    use futures_lite::StreamExt;
+    use zbus::{MatchRule, MessageStream, message::Type};
+
+    fn assert_wire_signature(message: &zbus::Message, signature: &str) {
+        let bytes = message.data().bytes();
+        let length = bytes[12..16].try_into().unwrap();
+        let length = match bytes[0] {
+            b'l' => u32::from_le_bytes(length),
+            b'B' => u32::from_be_bytes(length),
+            _ => panic!("invalid D-Bus byte order"),
+        } as usize;
+        // Inspect the encoded header: zbus normalizes outer parentheses when reading it.
+        let mut field = vec![8, 1, b'g', 0, signature.len() as u8];
+        field.extend_from_slice(signature.as_bytes());
+        field.push(0);
+        assert!(
+            bytes[16..16 + length]
+                .windows(field.len())
+                .any(|w| w == field)
+        );
+    }
+
+    #[test]
+    #[ignore = "requires a private D-Bus session; run with dbus-run-session"]
+    fn cache_signals_preserve_struct_arguments() {
+        futures_lite::future::block_on(async {
+            let conn = Connection::session().await.unwrap();
+            let rule = MatchRule::builder()
+                .msg_type(Type::Signal)
+                .interface("org.a11y.atspi.Cache")
+                .unwrap()
+                .path(cache_path())
+                .unwrap()
+                .build();
+            let mut signals = MessageStream::for_match_rule(rule, &conn, Some(2))
+                .await
+                .unwrap();
+            let executor = Executor::new();
+            let bus = Bus {
+                conn: conn.clone(),
+                _task: executor.spawn(std::future::pending(), "unused"),
+                socket_proxy: SocketProxy::new(&conn).await.unwrap(),
+                desktop: Arc::new(OnceLock::new()),
+            };
+            let item = CacheItem::default();
+            bus.emit_cache_signal("AddAccessible", &item).await.unwrap();
+            let added = signals.next().await.unwrap().unwrap();
+            assert_eq!(added.header().member().unwrap().as_str(), "AddAccessible");
+            assert_wire_signature(&added, "((so)(so)(so)iiassusau)");
+            let (received,): (CacheItem,) = added.body().deserialize().unwrap();
+            assert_eq!(received, item);
+
+            bus.emit_cache_signal("RemoveAccessible", &item.object)
+                .await
+                .unwrap();
+            let removed = signals.next().await.unwrap().unwrap();
+            assert_eq!(
+                removed.header().member().unwrap().as_str(),
+                "RemoveAccessible"
+            );
+            assert_wire_signature(&removed, "(so)");
+            let (received,): (ObjectRefOwned,) = removed.body().deserialize().unwrap();
+            assert_eq!(received, item.object);
+        });
+    }
+}
Dominant language
Rust
Stars
1.5k
Forks
122
Avg merge
1h 14m
Merged PRs (30d)
16

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from AccessKit/accesskit

All issues in AccessKit/accesskit

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.