use std::fs;
use std::io::{self, Read, Seek, Write};
use std::os::unix::fs::{DirBuilderExt, MetadataExt, OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf};
use sha2::{Digest, Sha256};
use super::hex;
use crate::error::Error;
use crate::process::BINARY_ENV;
#[derive(Debug, Clone)]
pub(crate) struct Layout {
pub root: PathBuf,
pub folders: Vec<PathBuf>,
pub file: PathBuf,
}
impl Layout {
pub(crate) fn new(root: &Path, version: &str, target: &str) -> Layout {
let sdk = root.join("megacode-agent-sdk");
let ver = sdk.join(version);
let tgt = ver.join(target);
Layout {
root: root.to_path_buf(),
file: tgt.join("harness"),
folders: vec![root.to_path_buf(), sdk, ver, tgt],
}
}
fn dir(&self) -> &Path {
self.folders.last().expect("four folders")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Cached {
Verified,
Missing,
Mismatch,
}
fn euid() -> u32 {
unsafe { libc::geteuid() }
}
fn refuse_folder(folder: &Path, why: &str) -> Error {
Error::BinaryUnavailable(format!(
"the cache folder {} {why}: it must be a real folder (not a symlink), owned by you, \
not writable by group or others. Fix the folder, or name a harness with \
Options::binary or {BINARY_ENV}",
folder.display()
))
}
fn refuse_ancestor(folder: &Path, why: &str) -> Error {
Error::BinaryUnavailable(format!(
"the folder {} above the cache {why}: every folder above the cache must be a real \
folder (not a symlink), owned by you or root, not writable by group or others unless \
sticky (as /tmp is). Fix the folder, or name a harness with Options::binary or \
{BINARY_ENV}",
folder.display()
))
}
fn refuse_file(file: &Path, why: &str) -> Error {
Error::BinaryUnavailable(format!(
"the cached harness {} {why}: it must be a regular file (not a symlink), owned by you, \
not writable by group or others. Remove it, or name a harness with Options::binary or \
{BINARY_ENV}",
file.display()
))
}
fn check_ancestors(root: &Path) -> Result<(), Error> {
if !root.is_absolute() {
return Err(refuse_folder(root, "is not an absolute path"));
}
let me = euid();
let mut above: Vec<&Path> = root.ancestors().skip(1).collect();
above.reverse();
for folder in above {
let m = fs::symlink_metadata(folder)
.map_err(|e| refuse_ancestor(folder, &format!("cannot be read ({e})")))?;
if m.file_type().is_symlink() {
return Err(refuse_ancestor(folder, "is a symlink"));
}
if !m.is_dir() {
return Err(refuse_ancestor(folder, "is not a folder"));
}
if m.uid() != me && m.uid() != 0 {
return Err(refuse_ancestor(folder, "is owned by another user"));
}
if m.mode() & 0o022 != 0 && m.mode() & 0o1000 == 0 {
return Err(refuse_ancestor(folder, "is writable by group or others"));
}
}
Ok(())
}
fn check_folder(folder: &Path) -> Result<bool, Error> {
let m = match fs::symlink_metadata(folder) {
Ok(m) => m,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false),
Err(e) => return Err(refuse_folder(folder, &format!("cannot be read ({e})"))),
};
if m.file_type().is_symlink() {
return Err(refuse_folder(folder, "is a symlink"));
}
if !m.is_dir() {
return Err(refuse_folder(folder, "is not a folder"));
}
if m.uid() != euid() {
return Err(refuse_folder(folder, "is owned by another user"));
}
if m.mode() & 0o022 != 0 {
return Err(refuse_folder(folder, "is writable by group or others"));
}
Ok(true)
}
pub(crate) fn check(layout: &Layout, executable_sha256: &str) -> Result<Cached, Error> {
check_ancestors(&layout.root)?;
for folder in &layout.folders {
if !check_folder(folder)? {
return Ok(Cached::Missing);
}
}
let file = &layout.file;
let mut open = fs::OpenOptions::new();
open.read(true)
.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK | libc::O_CLOEXEC);
let mut f = match open.open(file) {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Cached::Missing),
Err(e) if e.raw_os_error() == Some(libc::ELOOP) => {
return Err(refuse_file(file, "is a symlink"));
}
Err(e) => return Err(refuse_file(file, &format!("cannot be opened ({e})"))),
};
let m = f
.metadata()
.map_err(|e| refuse_file(file, &format!("cannot be read ({e})")))?;
if !m.is_file() {
return Err(refuse_file(file, "is not a regular file"));
}
if m.uid() != euid() {
return Err(refuse_file(file, "is owned by another user"));
}
if m.mode() & 0o022 != 0 {
return Err(refuse_file(file, "is writable by group or others"));
}
let digest = hash(&mut f).map_err(|e| refuse_file(file, &format!("cannot be read ({e})")))?;
Ok(if digest == executable_sha256 {
Cached::Verified
} else {
Cached::Mismatch
})
}
fn hash(f: &mut impl Read) -> io::Result<String> {
let mut h = Sha256::new();
let mut buf = vec![0u8; 256 * 1024];
loop {
let n = f.read(&mut buf)?;
if n == 0 {
return Ok(hex(&h.finalize()));
}
h.update(&buf[..n]);
}
}
pub(crate) fn install(
layout: &Layout,
executable: &[u8],
executable_sha256: &str,
) -> Result<(), Error> {
check_ancestors(&layout.root)?;
for folder in &layout.folders {
if check_folder(folder)? {
continue;
}
match fs::DirBuilder::new().mode(0o700).create(folder) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
Err(e) => return Err(refuse_folder(folder, &format!("cannot be made ({e})"))),
}
if !check_folder(folder)? {
return Err(refuse_folder(folder, "vanished after it was made"));
}
}
sweep_stale(layout.dir());
let tmp = layout.dir().join(format!(
".harness.{}.{:016x}.tmp",
std::process::id(),
crate::process::unique()
));
let written = write_checked(&tmp, executable, executable_sha256)
.and_then(|()| fs::rename(&tmp, &layout.file));
written.map_err(|e| {
let _ = fs::remove_file(&tmp);
Error::BinaryUnavailable(format!(
"cannot write the harness into {} ({e}); nothing was cached",
layout.dir().display()
))
})
}
const STALE: std::time::Duration = std::time::Duration::from_secs(60 * 60);
fn sweep_stale(dir: &Path) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if !(name.starts_with(".harness.") && name.ends_with(".tmp")) {
continue;
}
let path = entry.path();
let stale = fs::symlink_metadata(&path).is_ok_and(|m| {
m.is_file()
&& m.modified()
.is_ok_and(|t| t.elapsed().is_ok_and(|age| age > STALE))
});
if stale {
let _ = fs::remove_file(&path);
}
}
}
fn write_checked(tmp: &Path, executable: &[u8], executable_sha256: &str) -> io::Result<()> {
let mut f = fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.mode(0o700)
.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC)
.open(tmp)?;
f.write_all(executable)?;
f.sync_all()?;
f.rewind()?;
let digest = hash(&mut f)?;
if digest != executable_sha256 {
return Err(io::Error::other(format!(
"the written file's SHA-256 is {digest}, not the pinned {executable_sha256}"
)));
}
f.set_permissions(fs::Permissions::from_mode(0o755))?;
f.sync_all()
}