use std::fs::File;
use std::path::Path;
pub(crate) const MAX_MODEL_FILE_BYTES: u64 = 1024 * 1024 * 1024;
#[cfg(all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
))]
pub(crate) mod open_flags {
pub(crate) const O_NONBLOCK: i32 = 0o4_000;
#[cfg(target_arch = "x86_64")]
pub(crate) const O_NOFOLLOW: i32 = 0o400_000;
#[cfg(target_arch = "aarch64")]
pub(crate) const O_NOFOLLOW: i32 = 0o100_000;
#[cfg(target_arch = "x86_64")]
pub(crate) const O_DIRECTORY: i32 = 0o200_000;
#[cfg(target_arch = "aarch64")]
pub(crate) const O_DIRECTORY: i32 = 0o40_000;
pub(crate) const ELOOP: i32 = 40;
}
#[cfg(target_os = "macos")]
pub(crate) mod open_flags {
pub(crate) const O_NONBLOCK: i32 = 0x0004;
pub(crate) const O_NOFOLLOW: i32 = 0x0100;
pub(crate) const ELOOP: i32 = 62;
}
pub(crate) fn open_leaf_no_follow(leaf: &Path) -> std::io::Result<File> {
#[cfg(any(
all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
),
target_os = "macos"
))]
{
use std::os::unix::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
.read(true)
.custom_flags(open_flags::O_NOFOLLOW | open_flags::O_NONBLOCK)
.open(leaf)
}
#[cfg(not(any(
all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
),
target_os = "macos"
)))]
{
let _ = leaf;
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"このプラットフォームでは葉ファイルのシンボリックリンク追跡なし \
オープンを実装していないため、TOCTOU 対策として fail-closed で \
オープンを拒否します(Linux/macOS のみ対応)",
))
}
}
#[derive(Debug)]
pub(crate) enum LeafError {
Io(std::io::Error),
TooLarge { limit: u64 },
}
pub(crate) struct OpenedLeaf {
file: File,
len: u64,
}
impl OpenedLeaf {
pub(crate) fn len(&self) -> u64 {
self.len
}
pub(crate) fn read_exact_len(mut self) -> Result<Vec<u8>, LeafError> {
use std::io::Read;
let expected = usize::try_from(self.len).map_err(|_| {
LeafError::Io(std::io::Error::new(
std::io::ErrorKind::OutOfMemory,
"ファイル実長が usize に収まりません",
))
})?;
let mut bytes = Vec::new();
bytes
.try_reserve_exact(expected.saturating_add(1))
.map_err(|_| {
LeafError::Io(std::io::Error::new(
std::io::ErrorKind::OutOfMemory,
"読み取りバッファを確保できません",
))
})?;
self.file
.by_ref()
.take(self.len.saturating_add(1))
.read_to_end(&mut bytes)
.map_err(LeafError::Io)?;
if bytes.len() as u64 != self.len {
return Err(LeafError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"読み取り中にファイルサイズが変化しました(fstat 実長と不一致)",
)));
}
Ok(bytes)
}
}
pub(crate) fn open_leaf_checked(leaf: &Path, max: u64) -> Result<OpenedLeaf, LeafError> {
open_leaf_checked_with(leaf, max, || {})
}
fn open_leaf_checked_with(
leaf: &Path,
max: u64,
after_check: impl FnOnce(),
) -> Result<OpenedLeaf, LeafError> {
let invalid = |msg: &'static str| {
LeafError::Io(std::io::Error::new(std::io::ErrorKind::InvalidInput, msg))
};
let leaf_meta = std::fs::symlink_metadata(leaf).map_err(LeafError::Io)?;
if leaf_meta.file_type().is_symlink() || !leaf_meta.is_file() {
return Err(invalid(
"シンボリックリンクまたは通常ファイルでないため読み取りを拒否しました",
));
}
after_check();
let file = open_leaf_no_follow(leaf).map_err(LeafError::Io)?;
let open_meta = file.metadata().map_err(LeafError::Io)?;
if !open_meta.is_file() {
return Err(invalid("開いたハンドルが通常ファイルではありません"));
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if open_meta.dev() != leaf_meta.dev() || open_meta.ino() != leaf_meta.ino() {
return Err(invalid(
"検査後にファイルが差し替えられました(dev/ino 不一致)",
));
}
}
let len = open_meta.len();
if len > max {
return Err(LeafError::TooLarge { limit: max });
}
Ok(OpenedLeaf { file, len })
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn temp_dir(label: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"fandhe-ai-fs-guard-{}-{}-{label}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
std::fs::create_dir(&dir).expect("一時ディレクトリを作成できるはず");
dir
}
#[cfg(any(
all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
),
target_os = "macos"
))]
#[test]
fn open_leaf_checked_reads_regular_file_exactly() {
let dir = temp_dir("ok");
let path = dir.join("a.bin");
std::fs::File::create(&path)
.and_then(|mut f| f.write_all(b"hello"))
.expect("書き込めるはず");
let opened = open_leaf_checked(&path, 16).expect("開けるはず");
assert_eq!(opened.len(), 5);
assert_eq!(opened.read_exact_len().expect("読めるはず"), b"hello");
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(any(
all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
),
target_os = "macos"
))]
#[test]
fn open_leaf_checked_rejects_over_bound_without_reading() {
let dir = temp_dir("big");
let path = dir.join("a.bin");
std::fs::write(&path, vec![0u8; 17]).expect("書き込めるはず");
assert!(matches!(
open_leaf_checked(&path, 16),
Err(LeafError::TooLarge { limit: 16 })
));
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(not(any(
all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
),
target_os = "macos"
)))]
#[test]
fn open_leaf_checked_is_unsupported_on_non_supported_platform() {
let dir = temp_dir("unsupported");
let small = dir.join("small.bin");
std::fs::File::create(&small)
.and_then(|mut f| f.write_all(b"hello"))
.expect("書き込めるはず");
let big = dir.join("big.bin");
std::fs::write(&big, vec![0u8; 17]).expect("書き込めるはず");
assert!(matches!(
open_leaf_checked(&small, 16),
Err(LeafError::Io(e)) if e.kind() == std::io::ErrorKind::Unsupported
));
assert!(matches!(
open_leaf_checked(&big, 16),
Err(LeafError::Io(e)) if e.kind() == std::io::ErrorKind::Unsupported
));
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn open_leaf_checked_rejects_symlink_and_directory() {
let dir = temp_dir("sym");
let target = dir.join("target.bin");
std::fs::write(&target, b"x").expect("書き込めるはず");
let link = dir.join("link.bin");
std::os::unix::fs::symlink(&target, &link).expect("symlink を作れるはず");
assert!(matches!(
open_leaf_checked(&link, 16),
Err(LeafError::Io(e)) if e.kind() == std::io::ErrorKind::InvalidInput
));
assert!(matches!(
open_leaf_checked(&dir, 16),
Err(LeafError::Io(e)) if e.kind() == std::io::ErrorKind::InvalidInput
));
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(unix)]
#[test]
fn read_exact_len_detects_growth_after_open() {
let dir = temp_dir("grow");
let path = dir.join("a.bin");
std::fs::write(&path, b"abcd").expect("書き込めるはず");
let opened = open_leaf_checked(&path, 16).expect("開けるはず");
let mut appender = std::fs::OpenOptions::new()
.append(true)
.open(&path)
.expect("追記できるはず");
appender.write_all(b"efgh").expect("追記できるはず");
assert!(matches!(
opened.read_exact_len(),
Err(LeafError::Io(e)) if e.kind() == std::io::ErrorKind::InvalidData
));
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(any(
all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
),
target_os = "macos"
))]
fn swapped_leaf_setup(label: &str) -> (std::path::PathBuf, std::path::PathBuf) {
let dir = temp_dir(label);
let path = dir.join("leaf.bin");
std::fs::write(&path, b"original").expect("書き込めるはず");
(dir, path)
}
#[cfg(any(
all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
),
target_os = "macos"
))]
#[test]
fn open_leaf_checked_rejects_symlink_swapped_in_after_check() {
let (dir, path) = swapped_leaf_setup("swap-sym");
let outside = dir.join("outside.bin");
std::fs::write(&outside, b"original").expect("書き込めるはず");
let result = open_leaf_checked_with(&path, 16, || {
std::fs::remove_file(&path).expect("消せるはず");
std::os::unix::fs::symlink(&outside, &path).expect("symlink を作れるはず");
});
assert!(matches!(
result,
Err(LeafError::Io(e)) if e.raw_os_error() == Some(open_flags::ELOOP)
));
assert_eq!(std::fs::read(&outside).expect("読めるはず"), b"original");
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(any(
all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
),
target_os = "macos"
))]
#[test]
fn open_leaf_checked_rejects_regular_file_swapped_in_after_check() {
let (dir, path) = swapped_leaf_setup("swap-reg");
let keep = dir.join("keep.bin");
let result = open_leaf_checked_with(&path, 16, || {
std::fs::rename(&path, &keep).expect("退避できるはず");
std::fs::write(&path, b"replaced").expect("書き込めるはず");
});
assert!(matches!(
result,
Err(LeafError::Io(e)) if e.kind() == std::io::ErrorKind::InvalidInput
));
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(any(
all(
target_os = "linux",
any(target_arch = "x86_64", target_arch = "aarch64")
),
target_os = "macos"
))]
#[test]
fn open_leaf_checked_rejects_fifo_swapped_in_after_check_without_hanging() {
let (dir, path) = swapped_leaf_setup("swap-fifo");
let worker_path = path.clone();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let result = open_leaf_checked_with(&worker_path, 16, || {
std::fs::remove_file(&worker_path).expect("消せるはず");
let status = std::process::Command::new("mkfifo")
.arg(&worker_path)
.status()
.expect("mkfifo を起動できるはず");
assert!(status.success(), "mkfifo が成功するはず");
});
let _ = tx.send(result.map(|_| ()));
});
let result = rx
.recv_timeout(std::time::Duration::from_secs(10))
.expect("FIFO でもハングせず結果を返すはず");
assert!(matches!(
result,
Err(LeafError::Io(e)) if e.kind() == std::io::ErrorKind::InvalidInput
));
let _ = std::fs::remove_dir_all(&dir);
}
}