Skip to content

Harden local state storage boundaries#3438

Closed
Hmbown wants to merge 1 commit into
mainfrom
issue/v0865-storage-path-hardening
Closed

Harden local state storage boundaries#3438
Hmbown wants to merge 1 commit into
mainfrom
issue/v0865-storage-path-hardening

Conversation

@Hmbown

@Hmbown Hmbown commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Summary

  • confines task artifact and automation/run IDs before filesystem path construction
  • enables SQLite foreign keys on every state-store connection so thread deletes cascade as declared
  • creates file-backed secrets with private Unix mode before writing secret bytes
  • keeps explicit CODEWHALE_HOME isolated from ambient legacy state/config while preserving default-home legacy backfill and migration

Verification

  • cargo fmt --all -- --check
  • git diff --check
  • cargo test -p codewhale-tui --bin codewhale-tui --locked write_task_artifact_rejects_path_like_task_ids
  • cargo test -p codewhale-tui --bin codewhale-tui --locked automation_
  • cargo test -p codewhale-state --locked
  • cargo test -p codewhale-secrets --locked
  • cargo test -p codewhale-config --locked
  • cargo test -p codewhale-tui --bin codewhale-tui --locked (5117 passed, 2 ignored)

Risk

  • Existing todo/task/automation behavior is preserved for normal UUID IDs. Path-like IDs now fail before storage paths are built.
  • Default $HOME/.codewhale installs still read/migrate legacy .deepseek state; only explicit CODEWHALE_HOME is treated as an isolation boundary.

Confine task artifact and automation/run storage IDs before path construction, enable SQLite foreign keys for every state connection, create file-backed secrets with private mode at first write, and keep explicit CODEWHALE_HOME isolated from ambient legacy state.

Regression coverage exercises path-like task IDs, automation and run IDs, thread deletion cascades, secure file-store round trips, and explicit home isolation while preserving default legacy backfill/migration behavior.

Verification:

- cargo fmt --all -- --check

- git diff --check

- cargo test -p codewhale-tui --bin codewhale-tui --locked write_task_artifact_rejects_path_like_task_ids

- cargo test -p codewhale-tui --bin codewhale-tui --locked automation_

- cargo test -p codewhale-state --locked

- cargo test -p codewhale-secrets --locked

- cargo test -p codewhale-config --locked

- cargo test -p codewhale-tui --bin codewhale-tui --locked

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces isolation support via the CODEWHALE_HOME environment variable, enables SQLite foreign keys with cascading deletes, secures file writing permissions in the keyring store, and adds path traversal validation for storage IDs. Review feedback suggests ensuring default_tasks_dir() also respects CODEWHALE_HOME to maintain isolation, resolving a potential TOCTOU race condition in the keyring store by operating directly on the file descriptor, and deduplicating the path validation logic across modules.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

}

fn write_artifact(&self, task_id: &str, label: &str, content: &str) -> Result<PathBuf> {
validate_task_storage_id(task_id)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The default_tasks_dir() function (defined around line 1695) does not check the CODEWHALE_HOME environment variable, unlike default_automations_dir() in automation_manager.rs and default_state_db_path() in state/src/lib.rs. This breaks the isolation boundary of CODEWHALE_HOME for background tasks, causing tasks from different isolated environments to potentially collide in ~/.codewhale/tasks.

Please update default_tasks_dir() to check CODEWHALE_HOME:

pub fn default_tasks_dir() -> PathBuf {
    if let Ok(path) = std::env::var("DEEPSEEK_TASKS_DIR")
        && !path.trim().is_empty()
    {
        return PathBuf::from(path);
    }
    if let Some(home) = std::env::var_os("CODEWHALE_HOME").filter(|value| !value.is_empty()) {
        return PathBuf::from(home).join("tasks");
    }
    dirs::home_dir()
        .map(|home| default_tasks_dir_for_home(&home))
        .unwrap_or_else(|| PathBuf::from(".codewhale").join("tasks"))
}

Comment thread crates/secrets/src/lib.rs
Comment on lines 497 to 501
if let Ok(meta) = fs::metadata(&self.path) {
let mut perms = meta.permissions();
perms.set_mode(0o600);
let _ = fs::set_permissions(&self.path, perms);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

To avoid a potential Time-of-Check to Time-of-Use (TOCTOU) race condition and unnecessary path resolution syscalls, perform the metadata and permission operations directly on the opened file descriptor rather than querying the file path again.

Suggested change
if let Ok(meta) = fs::metadata(&self.path) {
let mut perms = meta.permissions();
perms.set_mode(0o600);
let _ = fs::set_permissions(&self.path, perms);
}
if let Ok(meta) = file.metadata() {
let mut perms = meta.permissions();
perms.set_mode(0o600);
let _ = file.set_permissions(perms);
}

validate_storage_component("task id", task_id)
}

fn validate_storage_component(label: &str, value: &str) -> Result<()> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Make validate_storage_component visible to the crate (pub(crate)) so that it can be reused in automation_manager.rs to eliminate duplicated validation logic.

pub(crate) fn validate_storage_component(label: &str, value: &str) -> Result<()> {

Comment on lines +776 to +795
fn validate_storage_id(label: &str, value: &str) -> Result<()> {
let value = value.trim();
if value.is_empty() {
bail!("{label} must not be empty");
}
if value.contains('/') || value.contains('\\') {
bail!("{label} must not contain path separators: {value}");
}
let mut components = Path::new(value).components();
let Some(component) = components.next() else {
bail!("{label} must not be empty");
};
if components.next().is_some() {
bail!("{label} must be a single path component: {value}");
}
if !matches!(component, std::path::Component::Normal(_)) {
bail!("{label} must be a normal path component: {value}");
}
Ok(())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Reuse the validate_storage_component helper function from task_manager.rs to avoid code duplication of this security-sensitive validation logic.

fn validate_storage_id(label: &str, value: &str) -> Result<()> {
    crate::task_manager::validate_storage_component(label, value)
}

@Hmbown

Hmbown commented Jun 23, 2026

Copy link
Copy Markdown
Owner Author

Closing this duplicate branch in favor of #3433.

#3433 already carries the same local-state hardening scope and now also includes the stricter pr_attempt_record ordering fix from this branch: task IDs are resolved through TaskManager before the diff artifact is written. Keeping one PR avoids splitting one hardening concern across two branches.

Relevant verification preserved on #3433:

  • cargo fmt --all -- --check
  • git diff --check
  • cargo test -p codewhale-tui --bin codewhale-tui --locked artifact_writer_rejects_path_component_task_ids
  • cargo test -p codewhale-tui --bin codewhale-tui --locked task_artifact_helper_requires_existing_manager_task_before_write
  • package-level state/secrets/config checks from the PR body

@Hmbown Hmbown closed this Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant