@@ -1,31 +1,73 @@ |
| 1 | 1 | //! User session management |
| 2 | 2 | //! |
| 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. |
| 4 | 5 | |
| 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; |
| 7 | 10 | 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 | +} |
| 11 | 45 | |
| 12 | 46 | /// User session wrapper |
| 13 | 47 | pub struct UserSession { |
| 14 | | - process: Child, |
| 48 | + pid: Pid, |
| 15 | 49 | username: String, |
| 16 | 50 | } |
| 17 | 51 | |
| 18 | 52 | impl UserSession { |
| 19 | 53 | /// Start a user session |
| 20 | 54 | /// |
| 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 | + /// |
| 21 | 61 | /// # Arguments |
| 22 | 62 | /// * `username` - The user to run the session as |
| 63 | + /// * `password` - The user's password for PAM authentication |
| 23 | 64 | /// * `session_cmd` - Command to execute (e.g., ["Hyprland"] or ["gar-session.sh"]) |
| 24 | 65 | /// * `session_type` - "x11" or "wayland" |
| 25 | 66 | /// * `display` - X11 display (e.g., ":0"), None for Wayland sessions |
| 26 | 67 | /// * `vt` - Virtual terminal number |
| 27 | 68 | pub fn start( |
| 28 | 69 | username: &str, |
| 70 | + password: &str, |
| 29 | 71 | session_cmd: &[String], |
| 30 | 72 | session_type: &str, |
| 31 | 73 | display: Option<&str>, |
@@ -33,115 +75,121 @@ impl UserSession { |
| 33 | 75 | ) -> Result<Self> { |
| 34 | 76 | let user = User::from_name(username) |
| 35 | 77 | .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))?; |
| 37 | 79 | |
| 38 | 80 | let home = user.dir.to_string_lossy().to_string(); |
| 39 | 81 | let shell = user.shell.to_string_lossy().to_string(); |
| 40 | 82 | let uid = user.uid; |
| 41 | 83 | let gid = user.gid; |
| 42 | | - let username_cstr = CString::new(username).context("Invalid username")?; |
| 43 | 84 | |
| 44 | 85 | let is_wayland = session_type == "wayland"; |
| 45 | 86 | |
| 46 | 87 | // Determine session command |
| 47 | 88 | let (cmd_path, cmd_args) = if session_cmd.is_empty() { |
| 48 | | - // Default: try gar-session.sh, fall back to shell |
| 49 | 89 | 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() { |
| 51 | 91 | (default_session.to_string(), vec![]) |
| 52 | 92 | } else { |
| 53 | 93 | (shell.clone(), vec!["-l".to_string()]) |
| 54 | 94 | } |
| 55 | 95 | } 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()) |
| 57 | 103 | }; |
| 58 | 104 | |
| 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 | + )); |
| 89 | 116 | } |
| 90 | | - cmd.env("XDG_SESSION_DESKTOP", "gar"); |
| 91 | | - cmd.env("XDG_CURRENT_DESKTOP", "gar"); |
| 92 | | - } |
| 93 | 117 | |
| 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))?; |
| 126 | 124 | |
| 127 | | - Ok(()) |
| 128 | | - }); |
| 129 | | - } |
| 125 | + use std::os::unix::io::IntoRawFd; |
| 126 | + Some(tty.into_raw_fd()) |
| 127 | + } else { |
| 128 | + None |
| 129 | + }; |
| 130 | 130 | |
| 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 | + ); |
| 132 | 135 | |
| 133 | | - tracing::info!( |
| 134 | | - username, |
| 135 | | - session = cmd_path, |
| 136 | + tracing::debug!( |
| 137 | + cmd = %cmd_path, |
| 138 | + args = ?cmd_args, |
| 136 | 139 | session_type, |
| 137 | | - pid = process.id(), |
| 138 | | - "Started user session" |
| 140 | + vt, |
| 141 | + "About to fork for session" |
| 139 | 142 | ); |
| 140 | 143 | |
| 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 | + } |
| 145 | 193 | } |
| 146 | 194 | |
| 147 | 195 | /// Get the username this session belongs to |
@@ -151,22 +199,257 @@ impl UserSession { |
| 151 | 199 | |
| 152 | 200 | /// Check if session is still running |
| 153 | 201 | 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 | + ) |
| 155 | 206 | } |
| 156 | 207 | |
| 157 | 208 | /// Wait for session to exit |
| 158 | 209 | 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 | + } |
| 166 | 243 | } |
| 167 | 244 | |
| 168 | 245 | /// Get the process ID |
| 169 | 246 | pub fn pid(&self) -> u32 { |
| 170 | | - self.process.id() |
| 247 | + self.pid.as_raw() as u32 |
| 171 | 248 | } |
| 172 | 249 | } |
| 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; |