@@ -21,6 +21,8 @@ use clap::Parser;
2121
2222use 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