Skip to content

Commit 5b9a50a

Browse files
committed
Harden logind session path decoding
1 parent 96897ae commit 5b9a50a

File tree

4 files changed

+97
-6
lines changed

4 files changed

+97
-6
lines changed

llm-docs/LLM-TODO.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ The project daemon is located at `src/daemon/` (Rust).
3939
# Notes
4040
- 2026-01-18: logind session monitoring failure is non-fatal; daemon continues without native terminal switching.
4141
- 2026-01-18: logind session resolution now falls back to the user’s `Display` session when `GetSessionByPID` reports no session (systemd user service with lingering).
42+
- 2026-01-19: logind object path parsing accepts object paths, single-field structures, or strings (robust reply decoding).

llm-docs/implementation-notes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ When all windows are closed (no window focused), the daemon switches to the defa
115115
The daemon watches `org.freedesktop.login1.Session.Active` on the system bus. When the session becomes inactive (Ctrl+Alt+F*), it applies the `on_native_terminal` rule if present, otherwise it behaves like an unfocused state. When the session becomes active again, it refreshes focus by querying the backend (GNOME GetFocus DBus, KDE script callback, Wayland/X11 active-window query).
116116

117117
Session resolution prefers `XDG_SESSION_ID`, otherwise `GetSessionByPID`. If the PID is not in a logind session (common for systemd user services with lingering), it falls back to the user’s `Display` session via `GetUserByPID` + `org.freedesktop.login1.User.Display`.
118+
Logind replies are decoded via `OwnedValue` to tolerate object paths returned as a direct value, a single-field structure, or a string.
118119

119120
If logind monitoring fails to start (no system bus, permissions, etc.), the daemon logs the error and continues without native terminal switching.
120121

src/daemon/main.rs

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use x11rb::protocol::xproto::{
4141
use x11rb::rust_connection::RustConnection;
4242
use zbus::Connection;
4343
use zbus::object_server::SignalEmitter;
44-
use zbus::zvariant::OwnedObjectPath;
44+
use zbus::zvariant::{OwnedObjectPath, OwnedValue, Structure, Value};
4545

4646
// Generated COSMIC protocols
4747
mod cosmic_workspace {
@@ -2058,18 +2058,22 @@ async fn resolve_logind_session_path(
20582058

20592059
if let Ok(session_id) = env::var("XDG_SESSION_ID") {
20602060
println!("[Logind] Using XDG_SESSION_ID={}", session_id);
2061-
let path: OwnedObjectPath = manager.call("GetSession", &(session_id)).await?;
2061+
let path = parse_logind_object_path(
2062+
manager.call::<_, _, OwnedValue>("GetSession", &(session_id)).await?,
2063+
"GetSession",
2064+
)?;
20622065
println!("[Logind] Using session path: {}", path.as_str());
20632066
return Ok(path);
20642067
}
20652068
println!("[Logind] XDG_SESSION_ID not set; resolving session via logind");
20662069

20672070
let pid = std::process::id();
20682071
match manager
2069-
.call::<_, _, OwnedObjectPath>("GetSessionByPID", &(pid))
2072+
.call::<_, _, OwnedValue>("GetSessionByPID", &(pid))
20702073
.await
20712074
{
2072-
Ok(path) => {
2075+
Ok(value) => {
2076+
let path = parse_logind_object_path(value, "GetSessionByPID")?;
20732077
println!("[Logind] Using session path: {}", path.as_str());
20742078
Ok(path)
20752079
}
@@ -2093,20 +2097,72 @@ fn is_logind_empty_object_path(path: &OwnedObjectPath) -> bool {
20932097
path.as_str() == LOGIND_EMPTY_OBJECT_PATH
20942098
}
20952099

2100+
fn parse_logind_object_path(
2101+
value: OwnedValue,
2102+
context: &str,
2103+
) -> Result<OwnedObjectPath, Box<dyn std::error::Error + Send + Sync>> {
2104+
let debug_value = format!("{:?}", value);
2105+
if let Ok(path) = OwnedObjectPath::try_from(value.try_clone()?) {
2106+
return Ok(path);
2107+
}
2108+
if let Ok(structure) = Structure::try_from(value.try_clone()?) {
2109+
if let Some(path) = parse_logind_object_path_from_structure(&structure) {
2110+
return Ok(path);
2111+
}
2112+
}
2113+
if let Ok(text) = String::try_from(value) {
2114+
return OwnedObjectPath::try_from(text).map_err(|error| {
2115+
format!(
2116+
"logind {} returned invalid object path string: {}",
2117+
context, error
2118+
)
2119+
.into()
2120+
});
2121+
}
2122+
Err(format!(
2123+
"logind {} returned unexpected value: {}",
2124+
context, debug_value
2125+
)
2126+
.into())
2127+
}
2128+
2129+
fn parse_logind_object_path_from_structure(structure: &Structure<'_>) -> Option<OwnedObjectPath> {
2130+
let fields = structure.fields();
2131+
if fields.len() != 1 {
2132+
return None;
2133+
}
2134+
logind_object_path_from_value(&fields[0])
2135+
}
2136+
2137+
fn logind_object_path_from_value(value: &Value<'_>) -> Option<OwnedObjectPath> {
2138+
match value {
2139+
Value::ObjectPath(path) => Some(OwnedObjectPath::from(path.clone())),
2140+
Value::Str(text) => OwnedObjectPath::try_from(text.as_str()).ok(),
2141+
Value::Value(inner) => logind_object_path_from_value(inner),
2142+
_ => None,
2143+
}
2144+
}
2145+
20962146
async fn resolve_logind_display_session_path(
20972147
manager: &zbus::Proxy<'_>,
20982148
connection: &Connection,
20992149
pid: u32,
21002150
) -> Result<OwnedObjectPath, Box<dyn std::error::Error + Send + Sync>> {
2101-
let user_path: OwnedObjectPath = manager.call("GetUserByPID", &(pid)).await?;
2151+
let user_path = parse_logind_object_path(
2152+
manager.call::<_, _, OwnedValue>("GetUserByPID", &(pid)).await?,
2153+
"GetUserByPID",
2154+
)?;
21022155
let user_proxy = zbus::Proxy::new(
21032156
connection,
21042157
LOGIND_BUS_NAME,
21052158
user_path,
21062159
LOGIND_USER_INTERFACE,
21072160
)
21082161
.await?;
2109-
let display: OwnedObjectPath = user_proxy.get_property("Display").await?;
2162+
let display = parse_logind_object_path(
2163+
user_proxy.get_property::<OwnedValue>("Display").await?,
2164+
"User.Display",
2165+
)?;
21102166
if is_logind_empty_object_path(&display) {
21112167
return Err("logind user has no display session".into());
21122168
}

src/daemon/tests.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1783,3 +1783,36 @@ fn test_logind_empty_object_path_detection() {
17831783
assert!(is_logind_empty_object_path(&empty));
17841784
assert!(!is_logind_empty_object_path(&non_empty));
17851785
}
1786+
1787+
#[test]
1788+
fn test_parse_logind_object_path_value() {
1789+
use zbus::zvariant::ObjectPath;
1790+
1791+
let path = ObjectPath::try_from("/org/freedesktop/login1/session/_1").unwrap();
1792+
let value = OwnedValue::from(path);
1793+
let parsed = parse_logind_object_path(value, "test").unwrap();
1794+
1795+
assert_eq!(parsed.as_str(), "/org/freedesktop/login1/session/_1");
1796+
}
1797+
1798+
#[test]
1799+
fn test_parse_logind_object_path_structure_single_field() {
1800+
use zbus::zvariant::{ObjectPath, StructureBuilder};
1801+
1802+
let path = ObjectPath::try_from("/org/freedesktop/login1/session/_1").unwrap();
1803+
let structure = StructureBuilder::new().add_field(path).build().unwrap();
1804+
let value = OwnedValue::try_from(structure).unwrap();
1805+
let parsed = parse_logind_object_path(value, "test").unwrap();
1806+
1807+
assert_eq!(parsed.as_str(), "/org/freedesktop/login1/session/_1");
1808+
}
1809+
1810+
#[test]
1811+
fn test_parse_logind_object_path_string() {
1812+
use zbus::zvariant::Str;
1813+
1814+
let value = OwnedValue::from(Str::from("/org/freedesktop/login1/session/_1"));
1815+
let parsed = parse_logind_object_path(value, "test").unwrap();
1816+
1817+
assert_eq!(parsed.as_str(), "/org/freedesktop/login1/session/_1");
1818+
}

0 commit comments

Comments
 (0)