Unix cache signals lose their structure argument and are rejected by AT-SPI
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
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from AccessKit/accesskit
-
Document sub-treesOpen
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 25/100
Maintainers usually reply within 1 day
-
Crash on macOS when the subclassing adapter is dropped while the view has key-value observersPossibly taken @RXNova claimed this 2 days ago. Open
Difficulty 3/5 1-2 days Newbie friendliness 35/100
Maintainers usually reply within 1 day
-
Difficulty 3/5 1-2 days Newbie friendliness 68/100
AccessKit/accesskit#802 · 1 comment ·
Maintainers usually reply within 1 day
All issues in AccessKit/accesskit
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
Maintainers usually reply within 1 day
-
V1 manifest lists written by Java lose added/existing/deleted file countsPossibly taken @kacper-li-airspace-intelligence claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
apache/iceberg-rust#3445 ·
Maintainers usually reply within 4 days
-
Slack integration: links on self-hosted domains never unfurl (parseCapShareUrl only accepts cap.so / cap.link)Possibly taken @mangeshraut712 claimed this today. Openbug
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
CapSoftware/Cap#2546 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
Maintainers usually reply within 1 day
-
area::tern-sync kind::cleanup triage::ready
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day