gardesk/gardm / c6fbe97

Browse files

session: Rewrite session launch with fork/exec and PAM

Wayland compositors like Hyprland use libseat to get device access
(DRM, input) through logind. This requires a properly registered
logind session, which pam_systemd creates during open_session().

The key insight is that PAM open_session() must be called in the
process that becomes the user session, not in the daemon. This commit
rewrites session launching to use manual fork/exec:

Child process (becomes user session):
1. Call setsid() to become session leader
2. Set XDG_SESSION_TYPE, XDG_VTNR, XDG_SEAT for pam_systemd
3. PAM authenticate() and open_session() - registers with logind
4. Drop privileges (initgroups, setgid, setuid)
5. Set up controlling TTY for Wayland (chown, TIOCSCTTY)
6. execve() the compositor

Parent process (daemon):
1. Wait for child with waitpid()
2. Handle session end, restart greeter

Other changes:
- Add resolve_command() to find executables in user PATH
- Use std::mem::forget() to prevent PAM client drop closing session
- Build environment variables as CStrings for execve()
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
c6fbe97c242cb4ded16af3747a9919429bff27ad
Parents
adee740
Tree
8a1646e

1 changed file

StatusFile+-
M gardmd/src/session.rs 382 99
gardmd/src/session.rsmodified
@@ -1,31 +1,73 @@
11
 //! User session management
22
 //!
3
-//! Launches and manages user desktop sessions.
3
+//! Launches and manages user desktop sessions using fork/exec with proper
4
+//! PAM session handling in the child process.
45
 
5
-use anyhow::{Context, Result};
6
-use nix::unistd::User;
6
+use anyhow::{anyhow, Context, Result};
7
+use nix::sys::wait::{waitpid, WaitStatus};
8
+use nix::unistd::{ForkResult, Pid, User};
9
+use pam::Client;
710
 use std::ffi::CString;
8
-use std::os::fd::AsRawFd;
9
-use std::os::unix::process::CommandExt;
10
-use std::process::{Child, Command, ExitStatus, Stdio};
11
+use std::os::unix::io::RawFd;
12
+use std::path::Path;
13
+use std::process::ExitStatus;
14
+
15
+use crate::auth::PAM_SERVICE_NAME;
16
+
17
+/// Standard PATH for session processes
18
+const SESSION_PATH: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
19
+
20
+/// Resolve a command to its full path by searching PATH directories
21
+fn resolve_command(cmd: &str, home: &str) -> String {
22
+    if cmd.starts_with('/') {
23
+        return cmd.to_string();
24
+    }
25
+
26
+    let search_path = format!(
27
+        "{}/.local/bin:{}/.cargo/bin:{}",
28
+        home, home, SESSION_PATH
29
+    );
30
+
31
+    for dir in search_path.split(':') {
32
+        let full_path = Path::new(dir).join(cmd);
33
+        if full_path.exists() && full_path.is_file() {
34
+            if let Ok(metadata) = full_path.metadata() {
35
+                use std::os::unix::fs::PermissionsExt;
36
+                if metadata.permissions().mode() & 0o111 != 0 {
37
+                    return full_path.to_string_lossy().to_string();
38
+                }
39
+            }
40
+        }
41
+    }
42
+
43
+    cmd.to_string()
44
+}
1145
 
1246
 /// User session wrapper
1347
 pub struct UserSession {
14
-    process: Child,
48
+    pid: Pid,
1549
     username: String,
1650
 }
1751
 
1852
 impl UserSession {
1953
     /// Start a user session
2054
     ///
55
+    /// This forks and in the child process:
56
+    /// 1. Authenticates with PAM and opens a session (registers with logind)
57
+    /// 2. Drops privileges to the target user
58
+    /// 3. Sets up TTY for Wayland sessions
59
+    /// 4. Execs the session command
60
+    ///
2161
     /// # Arguments
2262
     /// * `username` - The user to run the session as
63
+    /// * `password` - The user's password for PAM authentication
2364
     /// * `session_cmd` - Command to execute (e.g., ["Hyprland"] or ["gar-session.sh"])
2465
     /// * `session_type` - "x11" or "wayland"
2566
     /// * `display` - X11 display (e.g., ":0"), None for Wayland sessions
2667
     /// * `vt` - Virtual terminal number
2768
     pub fn start(
2869
         username: &str,
70
+        password: &str,
2971
         session_cmd: &[String],
3072
         session_type: &str,
3173
         display: Option<&str>,
@@ -33,115 +75,121 @@ impl UserSession {
3375
     ) -> Result<Self> {
3476
         let user = User::from_name(username)
3577
             .context("Failed to look up user")?
36
-            .ok_or_else(|| anyhow::anyhow!("User {} not found", username))?;
78
+            .ok_or_else(|| anyhow!("User {} not found", username))?;
3779
 
3880
         let home = user.dir.to_string_lossy().to_string();
3981
         let shell = user.shell.to_string_lossy().to_string();
4082
         let uid = user.uid;
4183
         let gid = user.gid;
42
-        let username_cstr = CString::new(username).context("Invalid username")?;
4384
 
4485
         let is_wayland = session_type == "wayland";
4586
 
4687
         // Determine session command
4788
         let (cmd_path, cmd_args) = if session_cmd.is_empty() {
48
-            // Default: try gar-session.sh, fall back to shell
4989
             let default_session = "/usr/local/bin/gar-session.sh";
50
-            if std::path::Path::new(default_session).exists() {
90
+            if Path::new(default_session).exists() {
5191
                 (default_session.to_string(), vec![])
5292
             } else {
5393
                 (shell.clone(), vec!["-l".to_string()])
5494
             }
5595
         } else {
56
-            (session_cmd[0].clone(), session_cmd[1..].to_vec())
96
+            let resolved = resolve_command(&session_cmd[0], &home);
97
+            tracing::debug!(
98
+                original = %session_cmd[0],
99
+                resolved = %resolved,
100
+                "Resolved session command"
101
+            );
102
+            (resolved, session_cmd[1..].to_vec())
57103
         };
58104
 
59
-        let mut cmd = Command::new(&cmd_path);
60
-        cmd.args(&cmd_args)
61
-            .env_clear()
62
-            .env("HOME", &home)
63
-            .env("USER", username)
64
-            .env("LOGNAME", username)
65
-            .env("SHELL", &shell)
66
-            .env("PATH", format!("{}/.local/bin:{}/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", home, home))
67
-            .env("XDG_VTNR", vt.to_string())
68
-            .env("XDG_SEAT", "seat0")
69
-            .env("XDG_SESSION_CLASS", "user")
70
-            .env("DBUS_SESSION_BUS_ADDRESS", format!("unix:path=/run/user/{}/bus", uid.as_raw()))
71
-            .env("XDG_RUNTIME_DIR", format!("/run/user/{}", uid.as_raw()))
72
-            .env("XDG_DATA_DIRS", "/usr/local/share:/usr/share")
73
-            .env("XDG_CONFIG_DIRS", "/etc/xdg")
74
-            .stdout(Stdio::inherit())
75
-            .stderr(Stdio::inherit());
76
-
77
-        // Session-type specific environment
78
-        if is_wayland {
79
-            cmd.env("XDG_SESSION_TYPE", "wayland");
80
-            // Wayland compositors create their own WAYLAND_DISPLAY
81
-            // Don't set DISPLAY - that's X11-specific
82
-            tracing::info!(vt, "Configuring Wayland session environment");
83
-        } else {
84
-            // X11 session
85
-            cmd.env("XDG_SESSION_TYPE", "x11");
86
-            if let Some(display) = display {
87
-                cmd.env("DISPLAY", display);
88
-                cmd.env("XAUTHORITY", format!("{}/.Xauthority", home));
105
+        // Prepare TTY for Wayland sessions (as root, before fork)
106
+        let tty_path = format!("/dev/tty{}", vt);
107
+        let tty_fd: Option<RawFd> = if is_wayland {
108
+            // Change TTY ownership to target user
109
+            let tty_cstr = CString::new(tty_path.as_str()).context("Invalid TTY path")?;
110
+            let ret = unsafe { libc::chown(tty_cstr.as_ptr(), uid.as_raw(), gid.as_raw()) };
111
+            if ret != 0 {
112
+                return Err(anyhow!(
113
+                    "Failed to chown TTY: {}",
114
+                    std::io::Error::last_os_error()
115
+                ));
89116
             }
90
-            cmd.env("XDG_SESSION_DESKTOP", "gar");
91
-            cmd.env("XDG_CURRENT_DESKTOP", "gar");
92
-        }
93117
 
94
-        // Set up process to run as the user
95
-        let tty_path = format!("/dev/tty{}", vt);
96
-        unsafe {
97
-            let home_for_closure = home.clone();
98
-            cmd.pre_exec(move || {
99
-                // Initialize supplementary groups
100
-                nix::unistd::initgroups(&username_cstr, gid)?;
101
-
102
-                // Set GID and UID
103
-                nix::unistd::setgid(gid)?;
104
-                nix::unistd::setuid(uid)?;
105
-
106
-                // Change to home directory
107
-                std::env::set_current_dir(&home_for_closure)?;
108
-
109
-                // For Wayland sessions, set up controlling TTY
110
-                // Wayland compositors need direct TTY access for input/output
111
-                if is_wayland {
112
-                    // Create new session (detach from parent's controlling terminal)
113
-                    libc::setsid();
114
-
115
-                    // Open the VT and make it our controlling terminal
116
-                    let tty = std::fs::OpenOptions::new()
117
-                        .read(true)
118
-                        .write(true)
119
-                        .open(&tty_path)?;
120
-
121
-                    // Set as controlling terminal (TIOCSCTTY)
122
-                    if libc::ioctl(tty.as_raw_fd(), libc::TIOCSCTTY, 0) < 0 {
123
-                        tracing::warn!("Failed to set controlling TTY, compositor may handle this");
124
-                    }
125
-                }
118
+            // Open TTY as root
119
+            let tty = std::fs::OpenOptions::new()
120
+                .read(true)
121
+                .write(true)
122
+                .open(&tty_path)
123
+                .context(format!("Failed to open TTY {}", tty_path))?;
126124
 
127
-                Ok(())
128
-            });
129
-        }
125
+            use std::os::unix::io::IntoRawFd;
126
+            Some(tty.into_raw_fd())
127
+        } else {
128
+            None
129
+        };
130130
 
131
-        let process = cmd.spawn().context("Failed to spawn session")?;
131
+        // Build environment for the session
132
+        let env_vars = build_session_env(
133
+            username, &home, &shell, uid.as_raw(), vt, session_type, display,
134
+        );
132135
 
133
-        tracing::info!(
134
-            username,
135
-            session = cmd_path,
136
+        tracing::debug!(
137
+            cmd = %cmd_path,
138
+            args = ?cmd_args,
136139
             session_type,
137
-            pid = process.id(),
138
-            "Started user session"
140
+            vt,
141
+            "About to fork for session"
139142
         );
140143
 
141
-        Ok(Self {
142
-            process,
143
-            username: username.to_string(),
144
-        })
144
+        // Fork!
145
+        match unsafe { nix::unistd::fork() } {
146
+            Ok(ForkResult::Parent { child }) => {
147
+                // Parent: close the TTY fd if we opened one
148
+                if let Some(fd) = tty_fd {
149
+                    unsafe { libc::close(fd) };
150
+                }
151
+
152
+                tracing::info!(
153
+                    username,
154
+                    session = %cmd_path,
155
+                    args = ?cmd_args,
156
+                    session_type,
157
+                    pid = child.as_raw(),
158
+                    "Started user session"
159
+                );
160
+
161
+                Ok(Self {
162
+                    pid: child,
163
+                    username: username.to_string(),
164
+                })
165
+            }
166
+            Ok(ForkResult::Child) => {
167
+                // Child process - this will exec or exit
168
+                // Note: we can't use tracing here as it's not fork-safe
169
+
170
+                // Run child process setup - this calls exec() or exit(), never returns
171
+                child_process_main(
172
+                    username,
173
+                    password,
174
+                    &home,
175
+                    uid,
176
+                    gid,
177
+                    &cmd_path,
178
+                    &cmd_args,
179
+                    &env_vars,
180
+                    is_wayland,
181
+                    tty_fd,
182
+                    vt,
183
+                    session_type,
184
+                )
185
+            }
186
+            Err(e) => {
187
+                if let Some(fd) = tty_fd {
188
+                    unsafe { libc::close(fd) };
189
+                }
190
+                Err(anyhow!("Fork failed: {}", e))
191
+            }
192
+        }
145193
     }
146194
 
147195
     /// Get the username this session belongs to
@@ -151,22 +199,257 @@ impl UserSession {
151199
 
152200
     /// Check if session is still running
153201
     pub fn is_running(&mut self) -> bool {
154
-        matches!(self.process.try_wait(), Ok(None))
202
+        matches!(
203
+            waitpid(self.pid, Some(nix::sys::wait::WaitPidFlag::WNOHANG)),
204
+            Ok(WaitStatus::StillAlive)
205
+        )
155206
     }
156207
 
157208
     /// Wait for session to exit
158209
     pub fn wait(&mut self) -> Result<ExitStatus> {
159
-        let status = self.process.wait().context("Failed to wait for session")?;
160
-        tracing::info!(
161
-            username = %self.username,
162
-            exit_code = ?status.code(),
163
-            "Session ended"
164
-        );
165
-        Ok(status)
210
+        loop {
211
+            match waitpid(self.pid, None) {
212
+                Ok(WaitStatus::Exited(_, code)) => {
213
+                    tracing::info!(
214
+                        username = %self.username,
215
+                        exit_code = code,
216
+                        "Session ended"
217
+                    );
218
+                    // Create ExitStatus from code
219
+                    return Ok(std::process::ExitStatus::from_raw(code << 8));
220
+                }
221
+                Ok(WaitStatus::Signaled(_, sig, _)) => {
222
+                    tracing::info!(
223
+                        username = %self.username,
224
+                        signal = ?sig,
225
+                        "Session killed by signal"
226
+                    );
227
+                    return Ok(std::process::ExitStatus::from_raw(128 + sig as i32));
228
+                }
229
+                Ok(_) => {
230
+                    // Other status (stopped, continued) - keep waiting
231
+                    continue;
232
+                }
233
+                Err(nix::errno::Errno::ECHILD) => {
234
+                    // Child already reaped
235
+                    tracing::info!(username = %self.username, "Session ended (already reaped)");
236
+                    return Ok(std::process::ExitStatus::from_raw(0));
237
+                }
238
+                Err(e) => {
239
+                    return Err(anyhow!("waitpid failed: {}", e));
240
+                }
241
+            }
242
+        }
166243
     }
167244
 
168245
     /// Get the process ID
169246
     pub fn pid(&self) -> u32 {
170
-        self.process.id()
247
+        self.pid.as_raw() as u32
171248
     }
172249
 }
250
+
251
+/// Build environment variables for the session
252
+fn build_session_env(
253
+    username: &str,
254
+    home: &str,
255
+    shell: &str,
256
+    uid: u32,
257
+    vt: u32,
258
+    session_type: &str,
259
+    display: Option<&str>,
260
+) -> Vec<CString> {
261
+    let mut env = vec![
262
+        format!("HOME={}", home),
263
+        format!("USER={}", username),
264
+        format!("LOGNAME={}", username),
265
+        format!("SHELL={}", shell),
266
+        format!("PATH={}/.local/bin:{}/.cargo/bin:{}", home, home, SESSION_PATH),
267
+        format!("XDG_VTNR={}", vt),
268
+        "XDG_SEAT=seat0".to_string(),
269
+        "XDG_SESSION_CLASS=user".to_string(),
270
+        format!("DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/{}/bus", uid),
271
+        format!("XDG_RUNTIME_DIR=/run/user/{}", uid),
272
+        "XDG_DATA_DIRS=/usr/local/share:/usr/share".to_string(),
273
+        "XDG_CONFIG_DIRS=/etc/xdg".to_string(),
274
+        format!("XDG_SESSION_TYPE={}", session_type),
275
+    ];
276
+
277
+    if session_type == "x11" {
278
+        if let Some(d) = display {
279
+            env.push(format!("DISPLAY={}", d));
280
+            env.push(format!("XAUTHORITY={}/.Xauthority", home));
281
+        }
282
+        env.push("XDG_SESSION_DESKTOP=gar".to_string());
283
+        env.push("XDG_CURRENT_DESKTOP=gar".to_string());
284
+    }
285
+
286
+    env.into_iter()
287
+        .filter_map(|s| CString::new(s).ok())
288
+        .collect()
289
+}
290
+
291
+/// Child process main function - handles PAM, privilege drop, TTY setup, and exec
292
+/// This function either calls exec() or exit() - it never returns
293
+fn child_process_main(
294
+    username: &str,
295
+    password: &str,
296
+    home: &str,
297
+    uid: nix::unistd::Uid,
298
+    gid: nix::unistd::Gid,
299
+    cmd_path: &str,
300
+    cmd_args: &[String],
301
+    env_vars: &[CString],
302
+    is_wayland: bool,
303
+    tty_fd: Option<RawFd>,
304
+    vt: u32,
305
+    session_type: &str,
306
+) -> ! {
307
+    // Create new session (detach from parent's controlling terminal)
308
+    // This must be done before PAM so pam_systemd sees us as a session leader
309
+    if is_wayland {
310
+        unsafe {
311
+            let sid = libc::setsid();
312
+            if sid < 0 {
313
+                eprintln!("[SESSION] setsid() failed: {}", std::io::Error::last_os_error());
314
+            }
315
+        }
316
+    }
317
+
318
+    // PAM authentication and session opening
319
+    // This registers the session with systemd-logind for device access
320
+    if let Err(e) = pam_authenticate_and_open_session(username, password, vt, session_type) {
321
+        eprintln!("[SESSION] PAM failed: {}", e);
322
+        std::process::exit(1);
323
+    }
324
+
325
+    // Initialize supplementary groups (must be done as root)
326
+    let username_cstr = match CString::new(username) {
327
+        Ok(c) => c,
328
+        Err(e) => {
329
+            eprintln!("[SESSION] Invalid username: {}", e);
330
+            std::process::exit(1);
331
+        }
332
+    };
333
+
334
+    if let Err(e) = nix::unistd::initgroups(&username_cstr, gid) {
335
+        eprintln!("[SESSION] initgroups failed: {}", e);
336
+        std::process::exit(1);
337
+    }
338
+
339
+    // Drop privileges
340
+    if let Err(e) = nix::unistd::setgid(gid) {
341
+        eprintln!("[SESSION] setgid failed: {}", e);
342
+        std::process::exit(1);
343
+    }
344
+
345
+    if let Err(e) = nix::unistd::setuid(uid) {
346
+        eprintln!("[SESSION] setuid failed: {}", e);
347
+        std::process::exit(1);
348
+    }
349
+
350
+    // Change to home directory
351
+    if std::env::set_current_dir(home).is_err() {
352
+        eprintln!("[SESSION] Failed to chdir to home");
353
+        // Non-fatal, continue
354
+    }
355
+
356
+    // Set up controlling TTY for Wayland sessions
357
+    if let Some(fd) = tty_fd {
358
+        unsafe {
359
+            // Set as controlling terminal (steal if necessary)
360
+            if libc::ioctl(fd, libc::TIOCSCTTY, 1) < 0 {
361
+                eprintln!(
362
+                    "[SESSION] TIOCSCTTY failed: {}",
363
+                    std::io::Error::last_os_error()
364
+                );
365
+                // Non-fatal for some compositors
366
+            }
367
+
368
+            // Set up stdin/stdout/stderr to the TTY
369
+            libc::dup2(fd, 0);
370
+            libc::dup2(fd, 1);
371
+            libc::dup2(fd, 2);
372
+
373
+            if fd > 2 {
374
+                libc::close(fd);
375
+            }
376
+        }
377
+    }
378
+
379
+    // Prepare exec arguments
380
+    let cmd_cstr = match CString::new(cmd_path) {
381
+        Ok(c) => c,
382
+        Err(e) => {
383
+            eprintln!("[SESSION] Invalid command path: {}", e);
384
+            std::process::exit(1);
385
+        }
386
+    };
387
+
388
+    let mut argv: Vec<CString> = vec![cmd_cstr.clone()];
389
+    for arg in cmd_args {
390
+        match CString::new(arg.as_str()) {
391
+            Ok(c) => argv.push(c),
392
+            Err(e) => {
393
+                eprintln!("[SESSION] Invalid argument: {}", e);
394
+                std::process::exit(1);
395
+            }
396
+        }
397
+    }
398
+
399
+    // Execute the session command
400
+    eprintln!("[SESSION] Executing: {} {:?}", cmd_path, cmd_args);
401
+
402
+    // execve replaces the process image
403
+    match nix::unistd::execve(&cmd_cstr, &argv, env_vars) {
404
+        Ok(_) => unreachable!(), // execve doesn't return on success
405
+        Err(e) => {
406
+            eprintln!("[SESSION] execve failed: {}", e);
407
+            std::process::exit(127);
408
+        }
409
+    }
410
+}
411
+
412
+/// Authenticate with PAM and open a session
413
+/// This must be called in the child process before dropping privileges
414
+fn pam_authenticate_and_open_session(
415
+    username: &str,
416
+    password: &str,
417
+    vt: u32,
418
+    session_type: &str,
419
+) -> Result<()> {
420
+    // Set environment variables BEFORE creating PAM client
421
+    // pam_systemd reads these to determine session type and VT
422
+    eprintln!("[PAM] Setting environment for pam_systemd: type={}, vt={}", session_type, vt);
423
+    std::env::set_var("XDG_SESSION_TYPE", session_type);
424
+    std::env::set_var("XDG_VTNR", vt.to_string());
425
+    std::env::set_var("XDG_SEAT", "seat0");
426
+    std::env::set_var("XDG_SESSION_CLASS", "user");
427
+
428
+    eprintln!("[PAM] Creating PAM client for user {}", username);
429
+
430
+    let mut client = Client::with_password(PAM_SERVICE_NAME)
431
+        .map_err(|e| anyhow!("Failed to create PAM client: {:?}", e))?;
432
+
433
+    client.conversation_mut().set_credentials(username, password);
434
+
435
+    eprintln!("[PAM] Authenticating...");
436
+    client
437
+        .authenticate()
438
+        .map_err(|e| anyhow!("PAM authentication failed: {:?}", e))?;
439
+
440
+    eprintln!("[PAM] Opening session...");
441
+    client
442
+        .open_session()
443
+        .map_err(|e| anyhow!("PAM open_session failed: {:?}", e))?;
444
+
445
+    eprintln!("[PAM] Session opened successfully");
446
+
447
+    // Keep the client alive - don't let it drop and close the session
448
+    // The session will be closed when the process exits
449
+    std::mem::forget(client);
450
+
451
+    Ok(())
452
+}
453
+
454
+// Trait implementation for ExitStatus::from_raw
455
+use std::os::unix::process::ExitStatusExt;