Phase 1: Repository & Infrastructure Setup
- Renamed directories: codex-rs -> llmx-rs, codex-cli -> llmx-cli
- Updated package.json files:
- Root: llmx-monorepo
- CLI: @llmx/llmx
- SDK: @llmx/llmx-sdk
- Updated pnpm workspace configuration
- Renamed binary: codex.js -> llmx.js
- Updated environment variables: CODEX_* -> LLMX_*
- Changed repository URLs to valknar/llmx
🤖 Generated with Claude Code
This commit is contained in:
294
llmx-rs/execpolicy/src/execv_checker.rs
Normal file
294
llmx-rs/execpolicy/src/execv_checker.rs
Normal file
@@ -0,0 +1,294 @@
|
||||
use std::borrow::Cow;
|
||||
use std::ffi::OsString;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::ArgType;
|
||||
use crate::Error::CannotCanonicalizePath;
|
||||
use crate::Error::CannotCheckRelativePath;
|
||||
use crate::Error::ReadablePathNotInReadableFolders;
|
||||
use crate::Error::WriteablePathNotInWriteableFolders;
|
||||
use crate::ExecCall;
|
||||
use crate::MatchedExec;
|
||||
use crate::Policy;
|
||||
use crate::Result;
|
||||
use crate::ValidExec;
|
||||
use path_absolutize::*;
|
||||
|
||||
macro_rules! check_file_in_folders {
|
||||
($file:expr, $folders:expr, $error:ident) => {
|
||||
if !$folders.iter().any(|folder| $file.starts_with(folder)) {
|
||||
return Err($error {
|
||||
file: $file.clone(),
|
||||
folders: $folders.to_vec(),
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub struct ExecvChecker {
|
||||
execv_policy: Policy,
|
||||
}
|
||||
|
||||
impl ExecvChecker {
|
||||
pub fn new(execv_policy: Policy) -> Self {
|
||||
Self { execv_policy }
|
||||
}
|
||||
|
||||
pub fn r#match(&self, exec_call: &ExecCall) -> Result<MatchedExec> {
|
||||
self.execv_policy.check(exec_call)
|
||||
}
|
||||
|
||||
/// The caller is responsible for ensuring readable_folders and
|
||||
/// writeable_folders are in canonical form.
|
||||
pub fn check(
|
||||
&self,
|
||||
valid_exec: ValidExec,
|
||||
cwd: &Option<OsString>,
|
||||
readable_folders: &[PathBuf],
|
||||
writeable_folders: &[PathBuf],
|
||||
) -> Result<String> {
|
||||
for (arg_type, value) in valid_exec
|
||||
.args
|
||||
.into_iter()
|
||||
.map(|arg| (arg.r#type, arg.value))
|
||||
.chain(
|
||||
valid_exec
|
||||
.opts
|
||||
.into_iter()
|
||||
.map(|opt| (opt.r#type, opt.value)),
|
||||
)
|
||||
{
|
||||
match arg_type {
|
||||
ArgType::ReadableFile => {
|
||||
let readable_file = ensure_absolute_path(&value, cwd)?;
|
||||
check_file_in_folders!(
|
||||
readable_file,
|
||||
readable_folders,
|
||||
ReadablePathNotInReadableFolders
|
||||
);
|
||||
}
|
||||
ArgType::WriteableFile => {
|
||||
let writeable_file = ensure_absolute_path(&value, cwd)?;
|
||||
check_file_in_folders!(
|
||||
writeable_file,
|
||||
writeable_folders,
|
||||
WriteablePathNotInWriteableFolders
|
||||
);
|
||||
}
|
||||
ArgType::OpaqueNonFile
|
||||
| ArgType::Unknown
|
||||
| ArgType::PositiveInteger
|
||||
| ArgType::SedCommand
|
||||
| ArgType::Literal(_) => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut program = valid_exec.program.to_string();
|
||||
for system_path in valid_exec.system_path {
|
||||
if is_executable_file(&system_path) {
|
||||
program = system_path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(program)
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_absolute_path(path: &str, cwd: &Option<OsString>) -> Result<PathBuf> {
|
||||
let file = PathBuf::from(path);
|
||||
let result = if file.is_relative() {
|
||||
match cwd {
|
||||
Some(cwd) => file.absolutize_from(cwd),
|
||||
None => return Err(CannotCheckRelativePath { file }),
|
||||
}
|
||||
} else {
|
||||
file.absolutize()
|
||||
};
|
||||
result
|
||||
.map(Cow::into_owned)
|
||||
.map_err(|error| CannotCanonicalizePath {
|
||||
file: path.to_string(),
|
||||
error: error.kind(),
|
||||
})
|
||||
}
|
||||
|
||||
fn is_executable_file(path: &str) -> bool {
|
||||
let file_path = Path::new(path);
|
||||
|
||||
if let Ok(metadata) = std::fs::metadata(file_path) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let permissions = metadata.permissions();
|
||||
|
||||
// Check if the file is executable (by checking the executable bit for the owner)
|
||||
return metadata.is_file() && (permissions.mode() & 0o111 != 0);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// TODO(mbolin): Check against PATHEXT environment variable.
|
||||
return metadata.is_file();
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
use crate::MatchedArg;
|
||||
use crate::PolicyParser;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
|
||||
fn setup(fake_cp: &Path) -> ExecvChecker {
|
||||
let source = format!(
|
||||
r#"
|
||||
define_program(
|
||||
program="cp",
|
||||
args=[ARG_RFILE, ARG_WFILE],
|
||||
system_path=[{fake_cp:?}]
|
||||
)
|
||||
"#
|
||||
);
|
||||
let parser = PolicyParser::new("#test", &source);
|
||||
let policy = parser.parse().unwrap();
|
||||
ExecvChecker::new(policy)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_valid_input_files() -> Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
|
||||
// Create an executable file that can be used with the system_path arg.
|
||||
let fake_cp = temp_dir.path().join("cp");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let fake_cp_file = std::fs::File::create(&fake_cp)?;
|
||||
let mut permissions = fake_cp_file.metadata()?.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
std::fs::set_permissions(&fake_cp, permissions)?;
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
std::fs::File::create(&fake_cp)?;
|
||||
}
|
||||
|
||||
// Create root_path and reference to files under the root.
|
||||
let root_path = temp_dir.path().to_path_buf();
|
||||
let source_path = root_path.join("source");
|
||||
let dest_path = root_path.join("dest");
|
||||
|
||||
let cp = fake_cp.to_str().unwrap().to_string();
|
||||
let root = root_path.to_str().unwrap().to_string();
|
||||
let source = source_path.to_str().unwrap().to_string();
|
||||
let dest = dest_path.to_str().unwrap().to_string();
|
||||
|
||||
let cwd = Some(root_path.clone().into());
|
||||
|
||||
let checker = setup(&fake_cp);
|
||||
let exec_call = ExecCall {
|
||||
program: "cp".into(),
|
||||
args: vec![source, dest.clone()],
|
||||
};
|
||||
let valid_exec = match checker.r#match(&exec_call).map_err(|e| anyhow!("{e:?}"))? {
|
||||
MatchedExec::Match { exec } => exec,
|
||||
unexpected => panic!("Expected a safe exec but got {unexpected:?}"),
|
||||
};
|
||||
|
||||
// No readable or writeable folders specified.
|
||||
assert_eq!(
|
||||
checker.check(valid_exec.clone(), &cwd, &[], &[]),
|
||||
Err(ReadablePathNotInReadableFolders {
|
||||
file: source_path,
|
||||
folders: vec![]
|
||||
}),
|
||||
);
|
||||
|
||||
// Only readable folders specified.
|
||||
assert_eq!(
|
||||
checker.check(
|
||||
valid_exec.clone(),
|
||||
&cwd,
|
||||
std::slice::from_ref(&root_path),
|
||||
&[]
|
||||
),
|
||||
Err(WriteablePathNotInWriteableFolders {
|
||||
file: dest_path.clone(),
|
||||
folders: vec![]
|
||||
}),
|
||||
);
|
||||
|
||||
// Both readable and writeable folders specified.
|
||||
assert_eq!(
|
||||
checker.check(
|
||||
valid_exec,
|
||||
&cwd,
|
||||
std::slice::from_ref(&root_path),
|
||||
std::slice::from_ref(&root_path)
|
||||
),
|
||||
Ok(cp.clone()),
|
||||
);
|
||||
|
||||
// Args are the readable and writeable folders, not files within the
|
||||
// folders.
|
||||
let exec_call_folders_as_args = ExecCall {
|
||||
program: "cp".into(),
|
||||
args: vec![root.clone(), root],
|
||||
};
|
||||
let valid_exec_call_folders_as_args = match checker
|
||||
.r#match(&exec_call_folders_as_args)
|
||||
.map_err(|e| anyhow!("{e:?}"))?
|
||||
{
|
||||
MatchedExec::Match { exec } => exec,
|
||||
_ => panic!("Expected a safe exec"),
|
||||
};
|
||||
assert_eq!(
|
||||
checker.check(
|
||||
valid_exec_call_folders_as_args,
|
||||
&cwd,
|
||||
std::slice::from_ref(&root_path),
|
||||
std::slice::from_ref(&root_path)
|
||||
),
|
||||
Ok(cp),
|
||||
);
|
||||
|
||||
// Specify a parent of a readable folder as input.
|
||||
let exec_with_parent_of_readable_folder = ValidExec {
|
||||
program: "cp".into(),
|
||||
args: vec![
|
||||
MatchedArg::new(
|
||||
0,
|
||||
ArgType::ReadableFile,
|
||||
root_path.parent().unwrap().to_str().unwrap(),
|
||||
)
|
||||
.map_err(|e| anyhow!("{e:?}"))?,
|
||||
MatchedArg::new(1, ArgType::WriteableFile, &dest).map_err(|e| anyhow!("{e:?}"))?,
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
checker.check(
|
||||
exec_with_parent_of_readable_folder,
|
||||
&cwd,
|
||||
std::slice::from_ref(&root_path),
|
||||
std::slice::from_ref(&dest_path)
|
||||
),
|
||||
Err(ReadablePathNotInReadableFolders {
|
||||
file: root_path.parent().unwrap().to_path_buf(),
|
||||
folders: vec![root_path.clone()]
|
||||
}),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user