use std::{
fs::File,
io::{self, Cursor, Read, Seek, Write},
path::PathBuf,
time::Duration,
};
use crate::common::resolve::AuthUser;
use crate::{
common::resolve::CurrentUser,
log::{auth_info, auth_warn},
};
use super::{
Process, WithProcess,
audit::secure_open_cookie_file,
file::FileLock,
interface::{DeviceId, ProcessId, UserId},
time::{ProcessCreateTime, SystemTime},
};
type BoolStorage = u8;
const SIZE_OF_TS: i64 = std::mem::size_of::<SystemTime>() as i64;
const SIZE_OF_BOOL: i64 = std::mem::size_of::<BoolStorage>() as i64;
const MOD_OFFSET: i64 = SIZE_OF_TS + SIZE_OF_BOOL;
#[derive(Debug)]
pub struct SessionRecordFile {
file: File,
timeout: Duration,
for_user: UserId,
}
impl SessionRecordFile {
const BASE_PATH: &'static str = "/var/run/sudo-rs/ts";
pub fn open_for_user(user: &CurrentUser, timeout: Duration) -> io::Result<Self> {
let uid = user.uid;
let mut path = PathBuf::from(Self::BASE_PATH);
path.push(uid.to_string());
SessionRecordFile::new(uid, secure_open_cookie_file(&path)?, timeout)
}
const FILE_VERSION: u16 = 2;
const MAGIC_NUM: u16 = 0x50D0;
const VERSION_OFFSET: u64 = Self::MAGIC_NUM.to_le_bytes().len() as u64;
const FIRST_RECORD_OFFSET: u64 =
Self::VERSION_OFFSET + Self::FILE_VERSION.to_le_bytes().len() as u64;
pub fn new(for_user: UserId, io: File, timeout: Duration) -> io::Result<Self> {
let mut session_records = SessionRecordFile {
file: io,
timeout,
for_user,
};
match session_records.read_magic()? {
Some(magic) if magic == Self::MAGIC_NUM => (),
x => {
if let Some(_magic) = x {
auth_info!("Session records file for user '{for_user}' is invalid, resetting");
}
session_records.init(Self::VERSION_OFFSET)?;
}
}
match session_records.read_version()? {
Some(v) if v == Self::FILE_VERSION => (),
x => {
if let Some(v) = x {
auth_info!(
"Session records file for user '{for_user}' has invalid version {v}, only file version {} is supported, resetting",
Self::FILE_VERSION
);
} else {
auth_info!(
"Session records file did not contain file version information, resetting"
);
}
session_records.init(Self::FIRST_RECORD_OFFSET)?;
}
}
Ok(session_records)
}
fn read_magic(&mut self) -> io::Result<Option<u16>> {
let mut magic_bytes = [0; std::mem::size_of::<u16>()];
match self.file.read_exact(&mut magic_bytes) {
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Ok(None),
Err(e) => Err(e),
Ok(()) => Ok(Some(u16::from_le_bytes(magic_bytes))),
}
}
fn read_version(&mut self) -> io::Result<Option<u16>> {
let mut version_bytes = [0; std::mem::size_of::<u16>()];
match self.file.read_exact(&mut version_bytes) {
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Ok(None),
Err(e) => Err(e),
Ok(()) => Ok(Some(u16::from_le_bytes(version_bytes))),
}
}
fn init(&mut self, offset: u64) -> io::Result<()> {
let lock = FileLock::exclusive(&self.file, false)?;
self.file.set_len(0)?;
self.file.rewind()?;
self.file.write_all(&Self::MAGIC_NUM.to_le_bytes())?;
self.file.write_all(&Self::FILE_VERSION.to_le_bytes())?;
self.file.seek(io::SeekFrom::Start(offset))?;
lock.unlock()?;
Ok(())
}
fn next_record(&mut self) -> io::Result<Option<SessionRecord>> {
let mut record_length_bytes = [0; std::mem::size_of::<u16>()];
let curr_pos = self.file.stream_position()?;
let record_length = match self.file.read_exact(&mut record_length_bytes) {
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
Err(e) => return Err(e),
Ok(()) => u16::from_le_bytes(record_length_bytes),
};
if record_length == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Found empty record",
));
}
let mut buf = vec![0; record_length as usize];
match self.file.read_exact(&mut buf) {
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {
auth_info!(
"Found incomplete record in session records file for {}, clearing rest of the file",
self.for_user
);
self.file.set_len(curr_pos)?;
return Ok(None);
}
Err(e) => return Err(e),
Ok(()) => (),
}
match SessionRecord::from_bytes(&buf) {
Err(_) => {
auth_info!(
"Found invalid record in session records file for {}, clearing rest of the file",
self.for_user
);
self.file.set_len(curr_pos)?;
Ok(None)
}
Ok(record) => Ok(Some(record)),
}
}
pub fn touch(&mut self, scope: RecordScope, auth_user: &AuthUser) -> io::Result<TouchResult> {
let lock = FileLock::exclusive(&self.file, false)?;
self.seek_to_first_record()?;
while let Some(record) = self.next_record()? {
if record.enabled && record.matches(&scope, auth_user) {
let now = SystemTime::now()?;
if record.written_between(now - self.timeout, now) {
self.file.seek(io::SeekFrom::Current(-MOD_OFFSET))?;
let new_time = SystemTime::now()?;
new_time.encode(&mut self.file)?;
self.file.seek(io::SeekFrom::Current(SIZE_OF_BOOL))?;
lock.unlock()?;
return Ok(TouchResult::Updated {
old_time: record.timestamp,
new_time,
});
} else {
lock.unlock()?;
return Ok(TouchResult::Outdated {
time: record.timestamp,
});
}
}
}
lock.unlock()?;
Ok(TouchResult::NotFound)
}
pub fn disable(&mut self, scope: RecordScope) -> io::Result<()> {
let lock = FileLock::exclusive(&self.file, false)?;
self.seek_to_first_record()?;
while let Some(record) = self.next_record()? {
if record.scope == scope {
self.file.seek(io::SeekFrom::Current(-SIZE_OF_BOOL))?;
write_bool(false, &mut self.file)?;
}
}
lock.unlock()?;
Ok(())
}
pub fn create(&mut self, scope: RecordScope, auth_user: &AuthUser) -> io::Result<CreateResult> {
let lock = FileLock::exclusive(&self.file, false)?;
self.seek_to_first_record()?;
while let Some(record) = self.next_record()? {
if record.matches(&scope, auth_user) {
self.file.seek(io::SeekFrom::Current(-MOD_OFFSET))?;
let new_time = SystemTime::now()?;
new_time.encode(&mut self.file)?;
write_bool(true, &mut self.file)?;
lock.unlock()?;
return Ok(CreateResult::Updated {
old_time: record.timestamp,
new_time,
});
}
}
let record = SessionRecord::new(scope, auth_user.uid)?;
self.file.seek(io::SeekFrom::End(0))?;
self.write_record(&record)?;
lock.unlock()?;
Ok(CreateResult::Created {
time: record.timestamp,
})
}
pub fn reset(&mut self) -> io::Result<()> {
self.init(0)
}
fn write_record(&mut self, record: &SessionRecord) -> io::Result<()> {
let bytes = record.as_bytes()?;
let record_length = bytes.len();
if record_length > u16::MAX as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"A record with an unexpectedly large size was created",
));
}
let record_length = record_length as u16;
self.file.write_all(&record_length.to_le_bytes())?;
self.file.write_all(&bytes)?;
Ok(())
}
fn seek_to_first_record(&mut self) -> io::Result<()> {
self.file
.seek(io::SeekFrom::Start(Self::FIRST_RECORD_OFFSET))?;
Ok(())
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum TouchResult {
Updated {
old_time: SystemTime,
new_time: SystemTime,
},
Outdated { time: SystemTime },
NotFound,
}
#[cfg_attr(not(test), allow(dead_code))]
pub enum CreateResult {
Updated {
old_time: SystemTime,
new_time: SystemTime,
},
Created { time: SystemTime },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecordScope {
Tty {
tty_device: DeviceId,
session_pid: ProcessId,
init_time: ProcessCreateTime,
},
Ppid {
group_pid: ProcessId,
session_pid: ProcessId,
init_time: ProcessCreateTime,
},
}
impl RecordScope {
fn encode(&self, target: &mut impl Write) -> std::io::Result<()> {
match self {
RecordScope::Tty {
tty_device,
session_pid,
init_time,
} => {
target.write_all(&[1u8])?;
let b = tty_device.inner().to_le_bytes();
target.write_all(&b)?;
let b = session_pid.inner().to_le_bytes();
target.write_all(&b)?;
init_time.encode(target)?;
}
RecordScope::Ppid {
group_pid,
session_pid,
init_time,
} => {
target.write_all(&[2u8])?;
let b = group_pid.inner().to_le_bytes();
target.write_all(&b)?;
let b = session_pid.inner().to_le_bytes();
target.write_all(&b)?;
init_time.encode(target)?;
}
}
Ok(())
}
fn decode(from: &mut impl Read) -> std::io::Result<RecordScope> {
let mut buf = [0; 1];
from.read_exact(&mut buf)?;
match buf[0] {
1 => {
let mut buf = [0; std::mem::size_of::<libc::dev_t>()];
from.read_exact(&mut buf)?;
let tty_device = libc::dev_t::from_le_bytes(buf);
let mut buf = [0; std::mem::size_of::<libc::pid_t>()];
from.read_exact(&mut buf)?;
let session_pid = libc::pid_t::from_le_bytes(buf);
let init_time = ProcessCreateTime::decode(from)?;
Ok(RecordScope::Tty {
tty_device: DeviceId::new(tty_device),
session_pid: ProcessId::new(session_pid),
init_time,
})
}
2 => {
let mut buf = [0; std::mem::size_of::<libc::pid_t>()];
from.read_exact(&mut buf)?;
let group_pid = libc::pid_t::from_le_bytes(buf);
let mut buf = [0; std::mem::size_of::<libc::pid_t>()];
from.read_exact(&mut buf)?;
let session_pid = libc::pid_t::from_le_bytes(buf);
let init_time = ProcessCreateTime::decode(from)?;
Ok(RecordScope::Ppid {
group_pid: ProcessId::new(group_pid),
session_pid: ProcessId::new(session_pid),
init_time,
})
}
x => Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Unexpected scope variant discriminator: {x}"),
)),
}
}
pub fn for_process(process: &Process) -> Option<RecordScope> {
let tty = Process::tty_device_id(WithProcess::Current);
if let Ok(Some(tty_device)) = tty {
if let Ok(init_time) = Process::starting_time(WithProcess::Other(process.session_id)) {
Some(RecordScope::Tty {
tty_device,
session_pid: process.session_id,
init_time,
})
} else {
auth_warn!("Could not get terminal foreground process starting time");
None
}
} else if let Some(parent_pid) = process.parent_pid {
if let Ok(init_time) = Process::starting_time(WithProcess::Other(parent_pid)) {
Some(RecordScope::Ppid {
group_pid: parent_pid,
session_pid: process.session_id,
init_time,
})
} else {
auth_warn!("Could not get parent process starting time");
None
}
} else {
None
}
}
}
fn write_bool(b: bool, target: &mut impl Write) -> io::Result<()> {
let s: BoolStorage = if b { 0xFF } else { 0x00 };
let bytes = s.to_le_bytes();
target.write_all(&bytes)?;
Ok(())
}
#[derive(Debug, PartialEq, Eq)]
pub struct SessionRecord {
scope: RecordScope,
auth_user: UserId,
timestamp: SystemTime,
enabled: bool,
}
impl SessionRecord {
fn new(scope: RecordScope, auth_user: UserId) -> io::Result<SessionRecord> {
Ok(Self::init(scope, auth_user, true, SystemTime::now()?))
}
fn init(
scope: RecordScope,
auth_user: UserId,
enabled: bool,
timestamp: SystemTime,
) -> SessionRecord {
SessionRecord {
scope,
auth_user,
timestamp,
enabled,
}
}
fn encode(&self, target: &mut impl Write) -> std::io::Result<()> {
self.scope.encode(target)?;
let buf = self.auth_user.inner().to_le_bytes();
target.write_all(&buf)?;
self.timestamp.encode(target)?;
write_bool(self.enabled, target)?;
Ok(())
}
fn decode(from: &mut impl Read) -> std::io::Result<SessionRecord> {
let scope = RecordScope::decode(from)?;
let mut buf = [0; std::mem::size_of::<libc::uid_t>()];
from.read_exact(&mut buf)?;
let auth_user = libc::uid_t::from_le_bytes(buf);
let auth_user = UserId::new(auth_user);
let timestamp = SystemTime::decode(from)?;
let mut buf = [0; std::mem::size_of::<BoolStorage>()];
from.read_exact(&mut buf)?;
let enabled = match BoolStorage::from_le_bytes(buf) {
0xFF => true,
0x00 => false,
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Invalid boolean value detected in input stream",
));
}
};
Ok(SessionRecord::init(scope, auth_user, enabled, timestamp))
}
pub fn as_bytes(&self) -> std::io::Result<Vec<u8>> {
let mut v = vec![];
self.encode(&mut v)?;
Ok(v)
}
pub fn from_bytes(data: &[u8]) -> std::io::Result<SessionRecord> {
let mut cursor = Cursor::new(data);
let record = SessionRecord::decode(&mut cursor)?;
if cursor.position() != data.len() as u64 {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Record size and record length did not match",
))
} else {
Ok(record)
}
}
pub fn matches(&self, scope: &RecordScope, auth_user: &AuthUser) -> bool {
self.scope == *scope && self.auth_user == auth_user.uid
}
pub fn written_between(&self, early_time: SystemTime, later_time: SystemTime) -> bool {
early_time <= later_time && self.timestamp >= early_time && self.timestamp <= later_time
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
use crate::common::{SudoPath, SudoString};
use crate::system::User;
use crate::system::interface::GroupId;
use crate::system::tests::tempfile;
static TEST_USER_ID: UserId = UserId::ROOT;
fn auth_user_from_uid(uid: libc::uid_t) -> AuthUser {
AuthUser::from_user_for_targetpw(User {
uid: UserId::new(uid),
gid: GroupId::new(0),
name: SudoString::new("dummy".to_owned()).unwrap(),
home: SudoPath::new(Path::new("/nonexistent").to_owned()).unwrap(),
shell: Path::new("/bin/sh").to_owned(),
groups: vec![],
})
}
#[test]
fn can_encode_and_decode() {
let tty_sample = SessionRecord::new(
RecordScope::Tty {
tty_device: DeviceId::new(10),
session_pid: ProcessId::new(42),
init_time: ProcessCreateTime::new(1, 0),
},
UserId::new(999),
)
.unwrap();
let mut bytes = tty_sample.as_bytes().unwrap();
let decoded = SessionRecord::from_bytes(&bytes).unwrap();
assert_eq!(tty_sample, decoded);
assert!(SessionRecord::from_bytes(&bytes[1..]).is_err());
bytes.push(0);
assert!(SessionRecord::from_bytes(&bytes).is_err());
let ppid_sample = SessionRecord::new(
RecordScope::Ppid {
group_pid: ProcessId::new(42),
session_pid: ProcessId::new(43),
init_time: ProcessCreateTime::new(151, 0),
},
UserId::new(123),
)
.unwrap();
let bytes = ppid_sample.as_bytes().unwrap();
let decoded = SessionRecord::from_bytes(&bytes).unwrap();
assert_eq!(ppid_sample, decoded);
}
#[test]
fn timestamp_record_matches_works() {
let init_time = ProcessCreateTime::new(1, 0);
let scope = RecordScope::Tty {
tty_device: DeviceId::new(12),
session_pid: ProcessId::new(1234),
init_time,
};
let tty_sample = SessionRecord::new(scope, UserId::new(675)).unwrap();
assert!(tty_sample.matches(&scope, &auth_user_from_uid(675)));
assert!(!tty_sample.matches(&scope, &auth_user_from_uid(789)));
assert!(!tty_sample.matches(
&RecordScope::Tty {
tty_device: DeviceId::new(20),
session_pid: ProcessId::new(1234),
init_time
},
&auth_user_from_uid(675),
));
assert!(!tty_sample.matches(
&RecordScope::Ppid {
group_pid: ProcessId::new(42),
session_pid: ProcessId::new(43),
init_time
},
&auth_user_from_uid(675),
));
std::thread::sleep(std::time::Duration::from_millis(1));
assert!(!tty_sample.matches(
&RecordScope::Tty {
tty_device: DeviceId::new(12),
session_pid: ProcessId::new(1234),
init_time: ProcessCreateTime::new(1, 1)
},
&auth_user_from_uid(675),
));
}
#[test]
fn timestamp_record_written_between_works() {
let some_time = SystemTime::now().unwrap() + Duration::from_secs(100 * 60);
let scope = RecordScope::Tty {
tty_device: DeviceId::new(12),
session_pid: ProcessId::new(1234),
init_time: ProcessCreateTime::new(0, 0),
};
let sample = SessionRecord::init(scope, UserId::new(1234), true, some_time);
let dur = Duration::from_secs(30);
assert!(sample.written_between(some_time, some_time));
assert!(sample.written_between(some_time, some_time + dur));
assert!(sample.written_between(some_time - dur, some_time));
assert!(!sample.written_between(some_time + dur, some_time - dur));
assert!(!sample.written_between(some_time + dur, some_time + dur + dur));
assert!(!sample.written_between(some_time - dur - dur, some_time - dur));
}
fn tempfile_with_data(data: &[u8]) -> io::Result<File> {
let mut file = tempfile()?;
file.write_all(data)?;
file.rewind()?;
Ok(file)
}
fn data_from_tempfile(mut f: File) -> io::Result<Vec<u8>> {
let mut v = vec![];
f.rewind()?;
f.read_to_end(&mut v)?;
Ok(v)
}
#[test]
fn session_record_file_header_checks() {
let c = tempfile_with_data(&[0xD0, 0x50, 0x02, 0x00]).unwrap();
let timeout = Duration::from_secs(30);
assert!(SessionRecordFile::new(TEST_USER_ID, c.try_clone().unwrap(), timeout).is_ok());
let v = data_from_tempfile(c).unwrap();
assert_eq!(&v[..], &[0xD0, 0x50, 0x02, 0x00]);
let c = tempfile_with_data(&[0xAB, 0xBA]).unwrap();
assert!(SessionRecordFile::new(TEST_USER_ID, c.try_clone().unwrap(), timeout).is_ok());
let v = data_from_tempfile(c).unwrap();
assert_eq!(&v[..], &[0xD0, 0x50, 0x02, 0x00]);
let c = tempfile_with_data(&[]).unwrap();
assert!(SessionRecordFile::new(TEST_USER_ID, c.try_clone().unwrap(), timeout).is_ok());
let v = data_from_tempfile(c).unwrap();
assert_eq!(&v[..], &[0xD0, 0x50, 0x02, 0x00]);
let c = tempfile_with_data(&[0xD0, 0x50, 0xAB, 0xBA, 0x0, 0x0]).unwrap();
assert!(SessionRecordFile::new(TEST_USER_ID, c.try_clone().unwrap(), timeout).is_ok());
let v = data_from_tempfile(c).unwrap();
assert_eq!(&v[..], &[0xD0, 0x50, 0x02, 0x00]);
}
#[test]
fn can_create_and_update_valid_file() {
let timeout = Duration::from_secs(30);
let c = tempfile_with_data(&[]).unwrap();
let mut srf =
SessionRecordFile::new(TEST_USER_ID, c.try_clone().unwrap(), timeout).unwrap();
let tty_scope = RecordScope::Tty {
tty_device: DeviceId::new(0),
session_pid: ProcessId::new(0),
init_time: ProcessCreateTime::new(0, 0),
};
let auth_user = auth_user_from_uid(2424);
let res = srf.create(tty_scope, &auth_user).unwrap();
let CreateResult::Created { time } = res else {
panic!("Expected record to be created");
};
std::thread::sleep(std::time::Duration::from_millis(1));
let second = srf.touch(tty_scope, &auth_user).unwrap();
let TouchResult::Updated { old_time, new_time } = second else {
panic!("Expected record to be updated");
};
assert_eq!(time, old_time);
assert_ne!(old_time, new_time);
std::thread::sleep(std::time::Duration::from_millis(1));
let res = srf.create(tty_scope, &auth_user).unwrap();
let CreateResult::Updated { old_time, new_time } = res else {
panic!("Expected record to be updated");
};
assert_ne!(old_time, new_time);
assert!(srf.reset().is_ok());
let data = data_from_tempfile(c).unwrap();
assert_eq!(&data, &[0xD0, 0x50, 0x02, 0x00]);
}
}