Skip to content

Commit a47181e

Browse files
more world-writable warning improvements (openai#6389)
3 improvements: 1. show up to 3 actual paths that are world-writable 2. do the scan/warning for Read-Only mode too, because it also applies there 3. remove the "Cancel" option since it doesn't always apply (like on startup)
1 parent 5beb616 commit a47181e

File tree

5 files changed

+155
-64
lines changed

5 files changed

+155
-64
lines changed

codex-rs/tui/src/app.rs

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,14 @@ impl App {
174174
skip_world_writable_scan_once: false,
175175
};
176176

177-
// On startup, if Auto mode (workspace-write) is active, warn about world-writable dirs on Windows.
177+
// On startup, if Auto mode (workspace-write) or ReadOnly is active, warn about world-writable dirs on Windows.
178178
#[cfg(target_os = "windows")]
179179
{
180180
let should_check = codex_core::get_platform_sandbox().is_some()
181181
&& matches!(
182182
app.config.sandbox_policy,
183183
codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. }
184+
| codex_core::protocol::SandboxPolicy::ReadOnly
184185
)
185186
&& !app
186187
.config
@@ -192,7 +193,7 @@ impl App {
192193
let env_map: std::collections::HashMap<String, String> = std::env::vars().collect();
193194
let tx = app.app_event_tx.clone();
194195
let logs_base_dir = app.config.codex_home.clone();
195-
Self::spawn_world_writable_scan(cwd, env_map, logs_base_dir, tx, false);
196+
Self::spawn_world_writable_scan(cwd, env_map, logs_base_dir, tx);
196197
}
197198
}
198199

@@ -386,9 +387,18 @@ impl App {
386387
AppEvent::OpenFullAccessConfirmation { preset } => {
387388
self.chat_widget.open_full_access_confirmation(preset);
388389
}
389-
AppEvent::OpenWorldWritableWarningConfirmation { preset } => {
390-
self.chat_widget
391-
.open_world_writable_warning_confirmation(preset);
390+
AppEvent::OpenWorldWritableWarningConfirmation {
391+
preset,
392+
sample_paths,
393+
extra_count,
394+
failed_scan,
395+
} => {
396+
self.chat_widget.open_world_writable_warning_confirmation(
397+
preset,
398+
sample_paths,
399+
extra_count,
400+
failed_scan,
401+
);
392402
}
393403
AppEvent::OpenFeedbackNote {
394404
category,
@@ -449,14 +459,15 @@ impl App {
449459
}
450460
AppEvent::UpdateSandboxPolicy(policy) => {
451461
#[cfg(target_os = "windows")]
452-
let policy_is_workspace_write = matches!(
462+
let policy_is_workspace_write_or_ro = matches!(
453463
policy,
454464
codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. }
465+
| codex_core::protocol::SandboxPolicy::ReadOnly
455466
);
456467

457468
self.chat_widget.set_sandbox_policy(policy);
458469

459-
// If sandbox policy becomes workspace-write, run the Windows world-writable scan.
470+
// If sandbox policy becomes workspace-write or read-only, run the Windows world-writable scan.
460471
#[cfg(target_os = "windows")]
461472
{
462473
// One-shot suppression if the user just confirmed continue.
@@ -466,15 +477,15 @@ impl App {
466477
}
467478

468479
let should_check = codex_core::get_platform_sandbox().is_some()
469-
&& policy_is_workspace_write
480+
&& policy_is_workspace_write_or_ro
470481
&& !self.chat_widget.world_writable_warning_hidden();
471482
if should_check {
472483
let cwd = self.config.cwd.clone();
473484
let env_map: std::collections::HashMap<String, String> =
474485
std::env::vars().collect();
475486
let tx = self.app_event_tx.clone();
476487
let logs_base_dir = self.config.codex_home.clone();
477-
Self::spawn_world_writable_scan(cwd, env_map, logs_base_dir, tx, false);
488+
Self::spawn_world_writable_scan(cwd, env_map, logs_base_dir, tx);
478489
}
479490
}
480491
}
@@ -628,28 +639,48 @@ impl App {
628639
env_map: std::collections::HashMap<String, String>,
629640
logs_base_dir: PathBuf,
630641
tx: AppEventSender,
631-
apply_preset_on_continue: bool,
632642
) {
643+
#[inline]
644+
fn normalize_windows_path_for_display(p: &std::path::Path) -> String {
645+
let canon = dunce::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
646+
canon.display().to_string().replace('/', "\\")
647+
}
633648
tokio::task::spawn_blocking(move || {
634-
if codex_windows_sandbox::preflight_audit_everyone_writable(
649+
let result = codex_windows_sandbox::preflight_audit_everyone_writable(
635650
&cwd,
636651
&env_map,
637652
Some(logs_base_dir.as_path()),
638-
)
639-
.is_err()
653+
);
654+
if let Ok(ref paths) = result
655+
&& !paths.is_empty()
640656
{
641-
if apply_preset_on_continue {
642-
if let Some(preset) = codex_common::approval_presets::builtin_approval_presets()
643-
.into_iter()
644-
.find(|p| p.id == "auto")
645-
{
646-
tx.send(AppEvent::OpenWorldWritableWarningConfirmation {
647-
preset: Some(preset),
648-
});
649-
}
657+
let as_strings: Vec<String> = paths
658+
.iter()
659+
.map(|p| normalize_windows_path_for_display(p))
660+
.collect();
661+
let sample_paths: Vec<String> = as_strings.iter().take(3).cloned().collect();
662+
let extra_count = if as_strings.len() > sample_paths.len() {
663+
as_strings.len() - sample_paths.len()
650664
} else {
651-
tx.send(AppEvent::OpenWorldWritableWarningConfirmation { preset: None });
652-
}
665+
0
666+
};
667+
668+
tx.send(AppEvent::OpenWorldWritableWarningConfirmation {
669+
preset: None,
670+
sample_paths,
671+
extra_count,
672+
failed_scan: false,
673+
});
674+
} else if result.is_err() {
675+
// Scan failed: still warn, but with no examples and mark as failed.
676+
let sample_paths: Vec<String> = Vec::new();
677+
let extra_count = 0usize;
678+
tx.send(AppEvent::OpenWorldWritableWarningConfirmation {
679+
preset: None,
680+
sample_paths,
681+
extra_count,
682+
failed_scan: true,
683+
});
653684
}
654685
});
655686
}

codex-rs/tui/src/app_event.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ pub(crate) enum AppEvent {
7979
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
8080
OpenWorldWritableWarningConfirmation {
8181
preset: Option<ApprovalPreset>,
82+
/// Up to 3 sample world-writable directories to display in the warning.
83+
sample_paths: Vec<String>,
84+
/// If there are more than `sample_paths`, this carries the remaining count.
85+
extra_count: usize,
86+
/// True when the scan failed (e.g. ACL query error) and protections could not be verified.
87+
failed_scan: bool,
8288
},
8389

8490
/// Show Windows Subsystem for Linux setup instructions for auto mode.

codex-rs/tui/src/chatwidget.rs

Lines changed: 89 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2045,9 +2045,48 @@ impl ChatWidget {
20452045
&& self.windows_world_writable_flagged()
20462046
{
20472047
let preset_clone = preset.clone();
2048+
// Compute sample paths for the warning popup.
2049+
let mut env_map: std::collections::HashMap<String, String> =
2050+
std::collections::HashMap::new();
2051+
for (k, v) in std::env::vars() {
2052+
env_map.insert(k, v);
2053+
}
2054+
let (sample_paths, extra_count, failed_scan) =
2055+
match codex_windows_sandbox::preflight_audit_everyone_writable(
2056+
&self.config.cwd,
2057+
&env_map,
2058+
Some(self.config.codex_home.as_path()),
2059+
) {
2060+
Ok(paths) if !paths.is_empty() => {
2061+
fn normalize_windows_path_for_display(
2062+
p: &std::path::Path,
2063+
) -> String {
2064+
let canon = dunce::canonicalize(p)
2065+
.unwrap_or_else(|_| p.to_path_buf());
2066+
canon.display().to_string().replace('/', "\\")
2067+
}
2068+
let as_strings: Vec<String> = paths
2069+
.iter()
2070+
.map(|p| normalize_windows_path_for_display(p))
2071+
.collect();
2072+
let samples: Vec<String> =
2073+
as_strings.iter().take(3).cloned().collect();
2074+
let extra = if as_strings.len() > samples.len() {
2075+
as_strings.len() - samples.len()
2076+
} else {
2077+
0
2078+
};
2079+
(samples, extra, false)
2080+
}
2081+
Err(_) => (Vec::new(), 0, true),
2082+
_ => (Vec::new(), 0, false),
2083+
};
20482084
vec![Box::new(move |tx| {
20492085
tx.send(AppEvent::OpenWorldWritableWarningConfirmation {
20502086
preset: Some(preset_clone.clone()),
2087+
sample_paths: sample_paths.clone(),
2088+
extra_count,
2089+
failed_scan,
20512090
});
20522091
})]
20532092
} else {
@@ -2111,7 +2150,7 @@ impl ChatWidget {
21112150
&env_map,
21122151
Some(self.config.codex_home.as_path()),
21132152
) {
2114-
Ok(()) => false,
2153+
Ok(paths) => !paths.is_empty(),
21152154
Err(_) => true,
21162155
}
21172156
}
@@ -2184,31 +2223,66 @@ impl ChatWidget {
21842223
pub(crate) fn open_world_writable_warning_confirmation(
21852224
&mut self,
21862225
preset: Option<ApprovalPreset>,
2226+
sample_paths: Vec<String>,
2227+
extra_count: usize,
2228+
failed_scan: bool,
21872229
) {
21882230
let (approval, sandbox) = match &preset {
21892231
Some(p) => (Some(p.approval), Some(p.sandbox.clone())),
21902232
None => (None, None),
21912233
};
21922234
let mut header_children: Vec<Box<dyn Renderable>> = Vec::new();
2193-
let title_line = Line::from("Auto mode has unprotected directories").bold();
2194-
let info_line = Line::from(vec![
2195-
"Some important directories on this system are world-writable. ".into(),
2196-
"The Windows sandbox cannot protect writes to these locations in Auto mode."
2235+
let mode_label = match self.config.sandbox_policy {
2236+
SandboxPolicy::WorkspaceWrite { .. } => "Auto mode",
2237+
SandboxPolicy::ReadOnly => "Read-Only mode",
2238+
_ => "Auto mode",
2239+
};
2240+
let title_line = Line::from("Unprotected directories found").bold();
2241+
let info_line = if failed_scan {
2242+
Line::from(vec![
2243+
"We couldn't complete the world-writable scan, so protections cannot be verified. "
2244+
.into(),
2245+
format!("The Windows sandbox cannot guarantee protection in {mode_label}.")
2246+
.fg(Color::Red),
2247+
])
2248+
} else {
2249+
Line::from(vec![
2250+
"Some important directories on this system are world-writable. ".into(),
2251+
format!(
2252+
"The Windows sandbox cannot protect writes to these locations in {mode_label}."
2253+
)
21972254
.fg(Color::Red),
2198-
]);
2255+
])
2256+
};
21992257
header_children.push(Box::new(title_line));
22002258
header_children.push(Box::new(
22012259
Paragraph::new(vec![info_line]).wrap(Wrap { trim: false }),
22022260
));
2261+
2262+
if !sample_paths.is_empty() {
2263+
// Show up to three examples and optionally an "and X more" line.
2264+
let mut lines: Vec<Line> = Vec::new();
2265+
lines.push(Line::from("Examples:").bold());
2266+
for p in &sample_paths {
2267+
lines.push(Line::from(format!(" - {p}")));
2268+
}
2269+
if extra_count > 0 {
2270+
lines.push(Line::from(format!("and {extra_count} more")));
2271+
}
2272+
header_children.push(Box::new(Paragraph::new(lines).wrap(Wrap { trim: false })));
2273+
}
22032274
let header = ColumnRenderable::with(header_children);
22042275

22052276
// Build actions ensuring acknowledgement happens before applying the new sandbox policy,
22062277
// so downstream policy-change hooks don't re-trigger the warning.
22072278
let mut accept_actions: Vec<SelectionAction> = Vec::new();
2208-
// Suppress the immediate re-scan once after user confirms continue.
2209-
accept_actions.push(Box::new(|tx| {
2210-
tx.send(AppEvent::SkipNextWorldWritableScan);
2211-
}));
2279+
// Suppress the immediate re-scan only when a preset will be applied (i.e., via /approvals),
2280+
// to avoid duplicate warnings from the ensuing policy change.
2281+
if preset.is_some() {
2282+
accept_actions.push(Box::new(|tx| {
2283+
tx.send(AppEvent::SkipNextWorldWritableScan);
2284+
}));
2285+
}
22122286
if let (Some(approval), Some(sandbox)) = (approval, sandbox.clone()) {
22132287
accept_actions.extend(Self::approval_preset_actions(approval, sandbox));
22142288
}
@@ -2222,36 +2296,21 @@ impl ChatWidget {
22222296
accept_and_remember_actions.extend(Self::approval_preset_actions(approval, sandbox));
22232297
}
22242298

2225-
let deny_actions: Vec<SelectionAction> = if preset.is_some() {
2226-
vec![Box::new(|tx| {
2227-
tx.send(AppEvent::OpenApprovalsPopup);
2228-
})]
2229-
} else {
2230-
Vec::new()
2231-
};
2232-
22332299
let items = vec![
22342300
SelectionItem {
22352301
name: "Continue".to_string(),
2236-
description: Some("Apply Auto mode for this session".to_string()),
2302+
description: Some(format!("Apply {mode_label} for this session")),
22372303
actions: accept_actions,
22382304
dismiss_on_select: true,
22392305
..Default::default()
22402306
},
22412307
SelectionItem {
22422308
name: "Continue and don't warn again".to_string(),
2243-
description: Some("Enable Auto mode and remember this choice".to_string()),
2309+
description: Some(format!("Enable {mode_label} and remember this choice")),
22442310
actions: accept_and_remember_actions,
22452311
dismiss_on_select: true,
22462312
..Default::default()
22472313
},
2248-
SelectionItem {
2249-
name: "Cancel".to_string(),
2250-
description: Some("Go back without enabling Auto mode".to_string()),
2251-
actions: deny_actions,
2252-
dismiss_on_select: true,
2253-
..Default::default()
2254-
},
22552314
];
22562315

22572316
self.bottom_pane.show_selection_view(SelectionViewParams {
@@ -2266,6 +2325,9 @@ impl ChatWidget {
22662325
pub(crate) fn open_world_writable_warning_confirmation(
22672326
&mut self,
22682327
_preset: Option<ApprovalPreset>,
2328+
_sample_paths: Vec<String>,
2329+
_extra_count: usize,
2330+
_failed_scan: bool,
22692331
) {
22702332
}
22712333

codex-rs/windows-sandbox-rs/src/audit.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use crate::token::world_sid;
22
use crate::winutil::to_wide;
3-
use anyhow::anyhow;
43
use anyhow::Result;
54
use std::collections::HashSet;
65
use std::ffi::c_void;
@@ -177,7 +176,7 @@ pub fn audit_everyone_writable(
177176
cwd: &Path,
178177
env: &std::collections::HashMap<String, String>,
179178
logs_base_dir: Option<&Path>,
180-
) -> Result<()> {
179+
) -> Result<Vec<PathBuf>> {
181180
let start = Instant::now();
182181
let mut flagged: Vec<PathBuf> = Vec::new();
183182
let mut seen: HashSet<String> = HashSet::new();
@@ -265,14 +264,7 @@ pub fn audit_everyone_writable(
265264
),
266265
logs_base_dir,
267266
);
268-
let mut list_err = String::new();
269-
for p in flagged {
270-
list_err.push_str(&format!("\n - {}", p.display()));
271-
}
272-
return Err(anyhow!(
273-
"Refusing to run: found directories writable by Everyone: {}",
274-
list_err
275-
));
267+
return Ok(flagged);
276268
}
277269
// Log success once if nothing flagged
278270
crate::logging::log_note(
@@ -281,7 +273,7 @@ pub fn audit_everyone_writable(
281273
),
282274
logs_base_dir,
283275
);
284-
Ok(())
276+
Ok(Vec::new())
285277
}
286278
// Fast mask-based check: does the DACL contain any ACCESS_ALLOWED ACE for
287279
// Everyone that includes generic or specific write bits? Skips inherit-only

0 commit comments

Comments
 (0)