Files
honey-biscuit-workshop/workshop-baker/src/prebake/security.rs
T
2026-06-09 23:09:57 +08:00

245 lines
8.9 KiB
Rust

use workshop_engine::ExecutionContext;
use crate::prebake::config::Security;
use crate::prebake::error::PrebakeError;
use crate::prebake::stage::PrebakeStage;
use privdrop::PrivDrop;
use std::ffi::CString;
use std::mem::MaybeUninit;
use std::str::FromStr;
const ROOT_UID: u32 = 0;
/// Resolves a username to its corresponding UID (User ID).
///
/// This function performs a thread-safe lookup of user information using the
/// `getpwnam_r` system call, which is the reentrant version of `getpwnam`.
/// It first attempts to find the user in the system password database,
/// and if not found, falls back to parsing the input as a numeric UID.
///
/// # Security Model
///
/// This function is part of the privilege management system. It is used
/// to resolve usernames to UIDs before privilege dropping operations,
/// ensuring that processes can transition to the correct user identity.
///
/// # Parameters
///
/// * `username` - The username to look up. Can be either:
/// - A textual username (e.g., `"vulcan"`, `"nobody"`)
/// - A numeric UID string (e.g., `"1000"`) for fallback resolution
///
/// # Returns
///
/// Returns `Result<u32, PrebakeError>`:
/// * `Ok(u32)` - The resolved UID on success
/// * `Err(PrebakeError::UserNotFound)` - User does not exist in the system
/// database and is not a valid numeric UID
///
/// # Error Handling
///
/// This function handles several error cases:
/// 1. **Invalid username format**: If the username contains a null byte,
/// `CString::new` will fail, returning `UserNotFound`.
/// 2. **Buffer too small**: If `getpwnam_r` returns `ERANGE`, the buffer
/// is dynamically resized and the lookup is retried.
/// 3. **User not found**: If `getpwnam_r` returns `0` with a null pointer,
/// the function attempts to parse `username` as a numeric UID.
/// If that also fails, `UserNotFound` is returned.
///
/// # Thread Safety
///
/// This function is thread-safe because it uses `getpwnam_r` instead of
/// the non-reentrant `getpwnam`. The `getpwnam_r` function stores its
/// result in the caller-provided buffer, avoiding race conditions in
/// multi-threaded contexts.
///
/// # Implementation Details
///
/// The function uses a dynamically growing buffer strategy:
/// 1. Start with a 4KB buffer (`bufsize = 4096`)
/// 2. If `getpwnam_r` returns `ERANGE` (buffer too small), double the buffer
/// 3. Retry until the buffer is large enough or a non-ERANGE error occurs
///
/// If the user is not found in the system database, the function attempts
/// to parse the `username` string directly as a numeric UID. This allows
/// configurations to specify UIDs directly (e.g., `"0"` for root) without
/// relying on the system's user database.
///
/// # Example
///
/// ```rust,ignore
/// // Resolve a username to UID
/// match lookup_uid_by_name("vulcan") {
/// Ok(uid) => println!("User vulcan has UID {}", uid),
/// Err(e) => eprintln!("Failed to resolve user: {}", e),
/// }
///
/// // Resolve a numeric UID string (fallback)
/// match lookup_uid_by_name("1000") {
/// Ok(uid) => println!("UID 1000 resolved to {}", uid),
/// Err(e) => eprintln!("Invalid UID: {}", e),
/// }
/// ```
///
/// # See Also
///
/// * [`prebake_drop_privilege`] - Uses this function to resolve target user before dropping privileges
/// * [`PrivDrop`](privdrop::PrivDrop) - The privilege dropping utility used after UID resolution
pub fn lookup_uid_by_name(username: &str) -> Result<u32, PrebakeError> {
let c_username = CString::new(username)
.map_err(|_| PrebakeError::UserNotFound(format!("Invalid username: {}", username)))?;
let mut pwd = MaybeUninit::<libc::passwd>::uninit();
let mut pwent = std::ptr::null_mut::<libc::passwd>();
let mut bufsize: usize = 4096;
let mut pwbuf = vec![0; bufsize];
loop {
let ret = unsafe {
libc::getpwnam_r(
c_username.as_ptr(),
pwd.as_mut_ptr(),
pwbuf.as_mut_ptr() as *mut libc::c_char,
pwbuf.len(),
&mut pwent,
)
};
if ret == libc::ERANGE {
bufsize *= 2;
pwbuf.resize(bufsize, 0);
} else {
break;
}
}
// NOTE: originally unsafe {pwent} here
// let pwent = pwent ;
if pwent.is_null() {
if let Ok(uid) = username.parse::<u32>() {
return Ok(uid);
}
return Err(PrebakeError::UserNotFound(format!(
"User '{}' not found and not a valid numeric UID",
username
)));
}
let uid = unsafe { (*pwent).pw_uid };
Ok(uid)
}
/// Drops process privileges if the current execution stage exceeds the allowed privilege boundary.
///
/// # Security Model
/// This function acts as a **privilege gate**. It ensures that once the pipeline progresses beyond
/// a certain stage (`stage_expected`), the process must no longer run with elevated (Root) privileges.
///
/// # Behavior
/// - **Conditional Execution**: Only attempts to drop privileges if `stage_current > stage_expected`.
/// - **Idempotent**: Safe to call multiple times. If the process is already running as the target user,
/// it returns `Ok(())` immediately.
/// - **State Validation**:
/// - If running as Root: Attempts to switch to the target user.
/// - If running as Target User: Returns success (no-op).
/// - If running as Other Non-Root User: Returns `Err(InvalidPrivilegeState)` because privilege
/// dropping is impossible from this state (you cannot switch users without Root).
///
/// # Logic Flow
/// 1. Check if `stage_current` exceeds `stage_expected`. If not, return `Ok(())` immediately.
/// 2. Resolve the target username to a UID.
/// 3. Compare Current Effective UID vs. Target UID:
/// - **Equal**: Log info and return `Ok(())`.
/// - **Current is Root (0)**: Proceed to drop privileges using `PrivDrop`.
/// - **Current is Neither**: Log error and return `Err(InvalidPrivilegeState)`.
/// 4. Verify the new UID after dropping (sanity check) and log success.
///
/// # Arguments
/// * `stage_current`: The pipeline stage currently being executed.
/// * `stage_expected`: The last stage allowed to run with elevated privileges.
/// * `user`: The target username to drop privileges to (e.g., "nobody", "builder").
///
/// # Returns
/// - `Ok(())`: Privileges were successfully dropped, or were already correct.
/// - `Err(PrebakeError)`:
/// - `UserNotFound`: The target username does not exist.
/// - `InvalidPrivilegeState`: Process is running as an unexpected non-root user.
/// - `PrivDropFailed`: System call to change user failed.
///
/// # Caller Responsibility
/// The caller must ensure that if a privilege drop is required, this function is invoked
/// while the process still holds Root privileges. The caller is also responsible for mapping
/// the returned `Err` to an appropriate exit code (e.g., 201) if termination is desired.
pub fn prebake_drop_privilege(
stage_current: PrebakeStage,
stage_expected: PrebakeStage,
user: &str,
ctx: &mut ExecutionContext,
) -> Result<(), PrebakeError> {
if ctx.dry_run {
return Ok(());
}
if stage_current > stage_expected {
let current_uid = unsafe { libc::geteuid() };
let target_uid = lookup_uid_by_name(user).inspect_err(|_e| {
log::error!(
"Cannot drop privileges: target user '{}' does not exist at stage {}",
user,
stage_current
);
log::error!("Aborting pipeline.");
})?;
if current_uid == target_uid {
log::debug!(
"Already running as target user (UID {}), skipping privilege drop",
target_uid
);
return Ok(());
}
if current_uid != ROOT_UID {
log::error!(
"Cannot drop privileges: current UID={} is neither Root (0) nor target UID={}",
current_uid,
target_uid
);
log::error!("Aborting pipeline.");
return Err(PrebakeError::InvalidPrivilegeState {
current_uid,
target_uid,
});
}
PrivDrop::default().user(user).apply().map_err(|e| {
log::error!(
"Failed to drop privileges to {} at stage {} (after {}): {}",
user,
stage_current,
stage_expected,
e
);
log::error!("Aborting pipeline.");
PrebakeError::PrivDropFailed(e.to_string())
})?;
let new_uid = unsafe { libc::geteuid() };
log::info!(
"Successfully dropped privileges from Root to UID {} (user: {})",
new_uid,
user
);
ctx.privileged = false;
}
Ok(())
}
pub fn get_drop_after(security: &Option<Security>) -> Result<PrebakeStage, String> {
security
.clone()
.and_then(|sec| sec.drop_after)
.map(|stage_str| PrebakeStage::from_str(&stage_str))
.unwrap_or(Ok(PrebakeStage::default()))
}