Skip to content

Commit 6a2fbdf

Browse files
committed
fix: validate startup config paths
1 parent 7954d02 commit 6a2fbdf

1 file changed

Lines changed: 185 additions & 25 deletions

File tree

src/cortex-cli/src/main.rs

Lines changed: 185 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ use clap::Parser;
2121

2222
use cortex_cli::cli::{Cli, ColorMode, Commands, LogLevel, dispatch_command};
2323

24+
const CORTEX_HOME_ENV: &str = "CORTEX_HOME";
25+
2426
/// Apply process hardening measures early in startup.
2527
#[cfg(not(debug_assertions))]
2628
#[ctor::ctor]
@@ -72,38 +74,196 @@ fn setup_debug_file_logging() -> Result<DebugLogGuard> {
7274
Ok(DebugLogGuard { _guard: guard })
7375
}
7476

75-
/// Check if CORTEX_HOME is writable.
76-
fn check_cortex_home_writable() -> Result<()> {
77+
#[derive(Debug)]
78+
struct StartupWriteCheckTarget {
79+
label: &'static str,
80+
source: &'static str,
81+
path: std::path::PathBuf,
82+
}
83+
84+
/// Resolve startup locations that need write access.
85+
fn startup_write_check_targets() -> Result<Vec<StartupWriteCheckTarget>> {
86+
let cortex_home = cortex_engine::config::find_cortex_home()?;
87+
let cortex_home_source = if std::env::var(cortex_engine::config::CORTEX_CONFIG_DIR_ENV)
88+
.is_ok_and(|v| !v.is_empty())
89+
{
90+
cortex_engine::config::CORTEX_CONFIG_DIR_ENV
91+
} else if std::env::var(CORTEX_HOME_ENV).is_ok_and(|v| !v.is_empty()) {
92+
CORTEX_HOME_ENV
93+
} else {
94+
"default Cortex home"
95+
};
96+
97+
let mut targets = vec![StartupWriteCheckTarget {
98+
label: "Cortex home",
99+
source: cortex_home_source,
100+
path: cortex_home.clone(),
101+
}];
102+
103+
if std::env::var(cortex_engine::config::CORTEX_CONFIG_ENV).is_ok_and(|v| !v.is_empty()) {
104+
let config_path = cortex_engine::config::get_config_path(&cortex_home);
105+
if let Some(config_parent) = config_path
106+
.parent()
107+
.filter(|parent| !parent.as_os_str().is_empty())
108+
{
109+
if config_parent != cortex_home {
110+
targets.push(StartupWriteCheckTarget {
111+
label: "Cortex config parent",
112+
source: cortex_engine::config::CORTEX_CONFIG_ENV,
113+
path: config_parent.to_path_buf(),
114+
});
115+
}
116+
}
117+
}
118+
119+
Ok(targets)
120+
}
121+
122+
fn check_startup_write_target(target: &StartupWriteCheckTarget) -> Result<()> {
77123
use anyhow::bail;
78124

79-
if let Ok(cortex_home_env) = std::env::var("CORTEX_HOME") {
80-
let cortex_home_path = std::path::Path::new(&cortex_home_env);
81-
if cortex_home_path.exists() {
82-
let test_file = cortex_home_path.join(".write_test");
83-
match std::fs::File::create(&test_file) {
84-
Ok(_) => {
85-
let _ = std::fs::remove_file(&test_file);
86-
}
87-
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
88-
bail!(
89-
"Cannot write to CORTEX_HOME: Permission denied\n\n\
90-
CORTEX_HOME is set to: {}\n\
91-
This directory exists but is not writable.\n\n\
92-
To fix this, either:\n\
93-
- Change permissions: chmod u+w {}\n\
94-
- Use a different directory: export CORTEX_HOME=/path/to/writable/dir\n\
95-
- Unset the variable to use default: unset CORTEX_HOME",
96-
cortex_home_env,
97-
cortex_home_env
98-
);
125+
if !target.path.exists() {
126+
return Ok(());
127+
}
128+
129+
if !target.path.is_dir() {
130+
bail!(
131+
"Cannot use {}: expected a directory\n\n\
132+
{} resolves to: {}\n\
133+
This path exists but is not a directory.\n\n\
134+
To fix this, point {} at a writable directory or unset it to use the default.",
135+
target.label,
136+
target.source,
137+
target.path.display(),
138+
target.source
139+
);
140+
}
141+
142+
let test_file = target.path.join(".write_test");
143+
match std::fs::File::create(&test_file) {
144+
Ok(_) => {
145+
let _ = std::fs::remove_file(&test_file);
146+
}
147+
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
148+
bail!(
149+
"Cannot write to {}: Permission denied\n\n\
150+
{} resolves to: {}\n\
151+
This directory exists but is not writable.\n\n\
152+
To fix this, point {} at a writable directory or update the directory permissions.",
153+
target.label,
154+
target.source,
155+
target.path.display(),
156+
target.source
157+
);
158+
}
159+
Err(_) => {
160+
// Other errors (e.g., disk full) - continue and let it fail later.
161+
}
162+
}
163+
164+
Ok(())
165+
}
166+
167+
/// Check if configured startup paths are writable.
168+
fn check_cortex_home_writable() -> Result<()> {
169+
for target in startup_write_check_targets()? {
170+
check_startup_write_target(&target)?;
171+
}
172+
173+
Ok(())
174+
}
175+
176+
#[cfg(test)]
177+
mod tests {
178+
use super::*;
179+
use serial_test::serial;
180+
use std::ffi::{OsStr, OsString};
181+
use tempfile::TempDir;
182+
183+
struct EnvVarGuard {
184+
key: &'static str,
185+
original: Option<OsString>,
186+
}
187+
188+
impl EnvVarGuard {
189+
fn set(key: &'static str, value: impl AsRef<OsStr>) -> Self {
190+
let original = std::env::var_os(key);
191+
// SAFETY: These tests are serialized and restore the environment on drop.
192+
unsafe { std::env::set_var(key, value) };
193+
Self { key, original }
194+
}
195+
196+
fn remove(key: &'static str) -> Self {
197+
let original = std::env::var_os(key);
198+
// SAFETY: These tests are serialized and restore the environment on drop.
199+
unsafe { std::env::remove_var(key) };
200+
Self { key, original }
201+
}
202+
}
203+
204+
impl Drop for EnvVarGuard {
205+
fn drop(&mut self) {
206+
match &self.original {
207+
Some(value) => {
208+
// SAFETY: These tests are serialized and restore the environment on drop.
209+
unsafe { std::env::set_var(self.key, value) };
99210
}
100-
Err(_) => {
101-
// Other errors (e.g., disk full) - continue and let it fail later
211+
None => {
212+
// SAFETY: These tests are serialized and restore the environment on drop.
213+
unsafe { std::env::remove_var(self.key) };
102214
}
103215
}
104216
}
105217
}
106-
Ok(())
218+
219+
#[test]
220+
#[serial]
221+
fn check_cortex_home_writable_rejects_cortex_config_dir_file() {
222+
let temp_dir = TempDir::new().unwrap();
223+
let config_dir_file = temp_dir.path().join("config-dir");
224+
std::fs::write(&config_dir_file, "not a directory").unwrap();
225+
226+
let _home = EnvVarGuard::remove("CORTEX_HOME");
227+
let _config = EnvVarGuard::remove(cortex_engine::config::CORTEX_CONFIG_ENV);
228+
let _config_dir = EnvVarGuard::set(
229+
cortex_engine::config::CORTEX_CONFIG_DIR_ENV,
230+
config_dir_file.as_os_str(),
231+
);
232+
233+
let err = check_cortex_home_writable()
234+
.expect_err("CORTEX_CONFIG_DIR should be validated when it overrides CORTEX_HOME")
235+
.to_string();
236+
237+
assert!(err.contains(cortex_engine::config::CORTEX_CONFIG_DIR_ENV));
238+
assert!(err.contains(&config_dir_file.display().to_string()));
239+
}
240+
241+
#[test]
242+
#[serial]
243+
fn check_cortex_home_writable_rejects_cortex_config_parent_file() {
244+
let temp_dir = TempDir::new().unwrap();
245+
let config_home = temp_dir.path().join("config-home");
246+
let config_parent_file = temp_dir.path().join("config-parent");
247+
std::fs::create_dir_all(&config_home).unwrap();
248+
std::fs::write(&config_parent_file, "not a directory").unwrap();
249+
250+
let _home = EnvVarGuard::remove("CORTEX_HOME");
251+
let _config_dir = EnvVarGuard::set(
252+
cortex_engine::config::CORTEX_CONFIG_DIR_ENV,
253+
config_home.as_os_str(),
254+
);
255+
let _config = EnvVarGuard::set(
256+
cortex_engine::config::CORTEX_CONFIG_ENV,
257+
config_parent_file.join("config.toml").as_os_str(),
258+
);
259+
260+
let err = check_cortex_home_writable()
261+
.expect_err("CORTEX_CONFIG parent should be validated when it overrides config path")
262+
.to_string();
263+
264+
assert!(err.contains(cortex_engine::config::CORTEX_CONFIG_ENV));
265+
assert!(err.contains(&config_parent_file.display().to_string()));
266+
}
107267
}
108268

109269
/// Check for updates in the background.

0 commit comments

Comments
 (0)