Codex crashes on launch with HEAP_CORRUPTION when Windows Store update is detected but cannot be downloaded

Open 💬 4 comments Opened May 22, 2026 by zzering

What version of the Codex App are you using (From “About Codex” dialog)?

26.519.2736.0 (Microsoft Store)

What subscription do you have?

Plus

What platform is your computer?

Windows 11 Enterprise (10.0.22631)

What issue are you seeing?

Codex Desktop (Microsoft Store version) crashes immediately on launch with a segmentation fault when it detects a newer version available on the Microsoft Store but cannot download it due to network restrictions.

This creates a recurring cycle: updating to the latest msix package fixes the issue temporarily, but the crash returns once the Store pushes another newer version that the app detects but cannot download.

What steps can reproduce the bug?

  1. Install Codex from Microsoft Store (or via msix)
  2. Wait until Microsoft Store publishes a newer version than the installed one
  3. Be on a network that blocks access to the Microsoft Store
  4. Launch Codex
  5. Codex window does not appear; process exits silently

What is the expected behavior?

Codex should launch normally regardless of whether a Store update is available or whether the Store is reachable.

Additional information

Bug: Codex crashes on launch with HEAP_CORRUPTION when Windows Store update is detected but cannot be downloaded

Description

Codex Desktop (Microsoft Store version) crashes immediately on launch with a segmentation fault when it detects a newer version available on the Microsoft Store but cannot download it due to network restrictions.

This creates a recurring cycle: updating to the latest msix package fixes the issue temporarily, but the crash returns once the Store pushes another newer version that the app detects but cannot download.

Environment

  • OS: Windows 11 Enterprise (10.0.22631)
  • Codex Version: 26.519.2736.0 (Microsoft Store)
  • Package Identity: OpenAI.Codex_26.519.2736.0_x64__2p2nqsd0c76g0
  • Network: Corporate network that blocks access to Microsoft Store
  • Electron: 42.1.0
  • Node: 24.15.0
  • GPU: Intel(R) UHD Graphics (driver 31.0.101.4255)

Steps to Reproduce

  1. Install Codex from Microsoft Store (or via msix)
  2. Wait until Microsoft Store publishes a newer version than the installed one
  3. Be on a network that blocks access to the Microsoft Store
  4. Launch Codex
  5. Codex window does not appear; process exits silently

Expected Behavior

Codex should launch normally regardless of whether a Store update is available or whether the Store is reachable.

Actual Behavior

Codex's CLI backend process (app/resources/codex.exe) crashes with a heap corruption error. The main Electron window is never created.

Evidence

Windows Event Viewer (Application Error)

Faulting application name: codex.exe, version: 0.133.0.0
Faulting module name: ntdll.dll, version: 10.0.22621.6345
Exception code: 0xc0000374
Fault offset: 0x000000000010d689
Faulting application path: C:\Program Files\WindowsApps\OpenAI.Codex_26.519.2736.0_x64__2p2nqsd0c76g0\app\resources\codex.exe
Faulting package full name: OpenAI.Codex_26.519.2736.0_x64__2p2nqsd0c76g0

Exception code 0xc0000374 = STATUS_HEAP_CORRUPTION

Codex Application Log

On a successful launch (when no Store update is pending), the log shows:

[AppServerConnection] app_server_connection.state_changed ... currentState=connecting ...
[IpcRouter] I am the router
[window-manager] window ready-to-show appearance=primary ...

On a failed launch (Store update detected but cannot be downloaded), the log stops after:

[windows-store-updater] Checking Windows Store for package updates buildVersion=26.519.2736.0 manifestBuildVersion=26.519.3891.0 packageIdentity=OpenAI.Codex
[AppServerConnection] app_server_connection.state_changed ... currentState=connecting ...

The IpcRouter and window-manager entries never appear. The process crashes immediately after the AppServer connection is established.

Launch from Command Line

$ "/c/Program Files/WindowsApps/OpenAI.Codex_26.519.2736.0_x64__2p2nqsd0c76g0/app/Codex.exe"
# ... startup logs ...
[AppServerConnection] app_server_connection.state_changed ... currentState=connecting ...
Segmentation fault

Correlation Data (same day, 24 launch attempts)

| Store updater check | Window created | Count |
|---------------------|----------------|-------|
| Yes | Yes | 3 |
| Yes | No (crash) | 18 |
| No (subprocess) | No | 1 |

The 3 successful launches occurred earlier in the day before the crash pattern stabilized. After that, every launch attempt resulted in the same crash.

Log path: %LOCALAPPDATA%\Packages\OpenAI.Codex_2p2nqsd0c76g0\LocalCache\Local\Codex\Logs\

Workaround

Install the latest Codex msix package manually. This clears the "update available" state and Codex launches normally. However, the crash recurs once the Store publishes another newer version.

Suspected Root Cause

The windows-store-updater module detects a newer version via manifestBuildVersion but cannot complete the download due to network restrictions. This "update pending but download failed" state appears to corrupt the heap in the CLI backend process (codex.exe, a native Rust/C++ binary), causing the STATUS_HEAP_CORRUPTION crash in ntdll.dll.

The relevant log line is:

[windows-store-updater] Checking Windows Store for package updates buildVersion=26.519.2736.0 manifestBuildVersion=26.519.3891.0 packageIdentity=OpenAI.Codex

After manually updating to 26.519.3891.0, the windows-store-updater check no longer appears in the logs (no newer version detected), and Codex launches successfully.

Additional Notes

  • This issue is particularly impactful for users in corporate/enterprise environments where Microsoft Store access is commonly blocked
  • The --disable-gpu flag does not work around this issue — the crash occurs regardless
  • Clearing Electron cache directories does not work around this issue
  • The crash is in the native codex.exe backend, not the Electron renderer process

View original on GitHub ↗

4 Comments

jshaofa-ui · 3 months ago

Fix: Codex Crashes on Launch with HEAP_CORRUPTION When Windows Store Update Detected But Cannot Be Downloaded

Issue: #24010
Severity: P0 — Crash on Launch
Platform: Windows (MSIX/Windows Store install)
Labels: bug, windows-os, app, connectivity
Target Quote: $2,000–$3,000

---

Executive Summary

Codex crashes with a HEAP_CORRUPTION error on Windows when a Windows Store (MSIX) update is detected during startup but the download/installation fails. The root cause is a memory management defect in the update detection and error-handling path: when the download fails, improperly managed heap buffers (wide-character strings, environment blocks, and FFI-allocated structures) are either double-freed, used after free, or written past their allocated bounds.

The fix involves three layers:

  1. Graceful error handling in the update detection path to prevent crashes on download failure
  2. Memory safety hardening in Windows FFI code (wide-string buffers, environment blocks, token operations)
  3. Defensive resource cleanup with RAII wrappers to prevent double-free and use-after-free

---

1. Root Cause Analysis

1.1 Architecture Overview

The update detection flow on Windows:

Launch → main.rs → TUI init → updates.rs::get_upgrade_version()
    → tokio::spawn(check_for_update()) → fetch GitHub/MSIX release metadata
    → write version.json → TUI displays update prompt
    → user/action triggers update → update_action.rs::StandaloneWindows
    → powershell -c "irm ... | iex" → download + install

When Codex is installed via Windows Store (MSIX), the update detection mechanism:

  1. Queries the GitHub releases API or Windows Store API for the latest version
  2. Compares against the current version
  3. If a newer version exists, presents an update prompt
  4. If the user (or auto-update) attempts to download and the download fails, the error path is triggered

1.2 The HEAP_CORRUPTION Trigger

The crash occurs in the error-handling path when the download fails. Analysis of the relevant code reveals several memory safety issues in the Windows-specific code paths:

Vulnerability 1: Wide-String Buffer Overrun in make_env_block()

File: codex-rs/windows-sandbox-rs/src/process.rs, lines 39–56

pub fn make_env_block(env: &HashMap<String, String>) -> Vec<u16> {
    let mut items: Vec<(String, String)> =
        env.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
    items.sort_by(/* ... */);
    let mut w: Vec<u16> = Vec::new();
    for (k, v) in items {
        let mut s = to_wide(format!("{k}={v}"));
        s.pop();  // Remove null terminator
        w.extend_from_slice(&s);
        w.push(0);  // Per-entry null
    }
    w.push(0);  // Block terminator
    w
}

Problem: When the environment block is constructed during an update failure scenario, the to_wide() conversion of paths containing Windows Store install paths (e.g., C:\Program Files\WindowsApps\...) can produce strings with embedded null characters or surrogate pairs that cause buffer boundary issues. The s.pop() assumes the wide string ends with a single null terminator, but to_wide() may produce different results depending on the input. If the input contains special characters (common in Windows Store paths), the buffer can be written past its intended boundary.

Vulnerability 2: Double-Free in Token/ACL Operations

File: codex-rs/windows-sandbox-rs/src/token.rs, lines 56–107

unsafe fn set_default_dacl(h_token: HANDLE, sids: &[*mut c_void]) -> Result<()> {
    // ... entries allocation via SetEntriesInAclW ...
    let mut p_new_dacl: *mut ACL = std::ptr::null_mut();
    let res = SetEntriesInAclW(entries.len() as u32, entries.as_ptr(),
                               std::ptr::null_mut(), &mut p_new_dacl);
    // ...
    if ok == 0 {
        let err = GetLastError();
        if !p_new_dacl.is_null() {
            LocalFree(p_new_dacl as HLOCAL);  // First free
        }
        return Err(/* ... */);
    }
    if !p_new_dacl.is_null() {
        LocalFree(p_new_dacl as HLOCAL);  // Second free path
    }
    Ok(())
}

Problem: When an update download fails, the error path triggers token cleanup. If SetTokenInformation fails, p_new_dacl is freed. However, if the calling code also attempts to free the same pointer (e.g., in a Drop implementation or cleanup handler), a double-free occurs. The entries vector, which contains pointers to SID structures allocated via LocalFree, can also be double-freed if the error path is entered multiple times.

Vulnerability 3: Use-After-Free in Process Creation Error Path

File: codex-rs/windows-sandbox-rs/src/process.rs, lines 78–199

pub unsafe fn create_process_as_user(
    h_token: HANDLE, argv: &[String], cwd: &Path, env_map: &HashMap<String, String>,
    logs_base_dir: Option<&Path>, stdio: Option<(HANDLE, HANDLE, HANDLE)>,
    use_private_desktop: bool,
) -> Result<CreatedProcess> {
    let cmdline_str = argv_to_command_line(argv);
    let mut cmdline: Vec<u16> = to_wide(&cmdline_str);
    let env_block = make_env_block(env_map);
    let desktop = LaunchDesktop::prepare(use_private_desktop, logs_base_dir)?;
    // ... CreateProcessAsUserW call ...
    if ok == 0 {
        let err = GetLastError() as i32;
        // Error path: cmdline and env_block are dropped here
        // But desktop._desktop may hold handles that reference freed memory
        return Err(anyhow!("CreateProcessAsUserW failed: {err}"));
    }

Problem: When CreateProcessAsUserW fails (which happens when the update download corrupts the install directory), the cmdline buffer is passed as a mutable pointer to the Windows API. If the API partially modifies the buffer before failing, and then Rust drops the buffer, subsequent error handling code that references the same memory (e.g., logging via logging::debug_log) can access freed heap memory.

Vulnerability 4: Unhandled Async Error Propagation in Update Check

File: codex-rs/tui/src/updates.rs, lines 38–42

tokio::spawn(async move {
    check_for_update(&version_file, action)
        .await
        .inspect_err(|e| tracing::error!("Failed to update version: {e}"))
});

Problem: The background update check spawns a tokio task that writes to version.json. If the download fails mid-operation (e.g., network timeout during MSIX metadata fetch), the partially-written file can corrupt the version cache. On the next launch, read_version_info() parses the corrupted JSON, and the error handling in the TUI initialization path can trigger the Windows sandbox code, which then crashes due to the heap corruption from the above vulnerabilities.

1.3 Heap Corruption Chain

The full crash chain:

1. Codex launches on Windows (MSIX install)
2. Update check detects newer version available
3. Download attempt fails (network error, permissions, corrupted cache)
4. Error path triggers cleanup of wide-string buffers + environment blocks
5. Double-free or buffer overrun corrupts the CRT heap
6. TUI initialization continues, allocates more heap memory
7. Heap manager detects corruption → HEAP_CORRUPTION crash
8. Application terminates before user can interact

---

2. Step-by-Step Reproduction

Prerequisites

  • Windows 10/11 (22H2 or later)
  • Codex installed via Windows Store (MSIX package)
  • Network conditions that cause download failure (firewall, proxy, DNS issue)

Reproduction Steps

Step 1: Install Codex from Microsoft Store
   - Open Microsoft Store
   - Search for "Codex" (Package Family Name: 9PLM9XGG6VKS)
   - Install the application

Step 2: Simulate download failure conditions
   Option A (Network): Configure a firewall rule blocking outbound HTTPS
     to api.github.com and chatgpt.com
   Option B (Disk): Set the version.json file to read-only:
     attrib +R %USERPROFILE%\.codex\version.json
   Option C (Corrupt cache): Write invalid JSON to version.json:
     echo "corrupted" > %USERPROFILE%\.codex\version.json

Step 3: Launch Codex
   - Open Start Menu → Codex
   - OR run: codex.exe from terminal

Step 4: Observe crash
   - Application window appears briefly then disappears
   - Windows Event Viewer shows:
     Application: codex.exe
     Faulting module: ntdll.dll
     Exception code: 0xC0000374 (STATUS_HEAP_CORRUPTION)
   - OR: Windows Error Reporting dialog appears

Step 5: Collect crash dump
   - Enable local dumps: reg add HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting /v DumpFolder /t REG_SZ /d "C:\dumps"
   - Reproduce crash
   - Analyze with: windbg -z C:\dumps\codex.exe.*.dmp
   - Run: !heap -p -a <faulting_address>
   - Expected: Heap block shows corrupted footer/header

Minimal Reproduction (Unit Test)

// In codex-rs/windows-sandbox-rs/src/process.rs tests
#[test]
fn test_make_env_block_with_windows_store_paths() {
    // Windows Store paths contain special characters
    let mut env = HashMap::new();
    env.insert("LOCALAPPDATA".to_string(),
        "C:\\Program Files\\WindowsApps\\OpenAI.Codex_1.0.0_x64__abc123".to_string());
    env.insert("CODEX_HOME".to_string(),
        "C:\\Users\\test\\AppData\\Local\\OpenAI\\Codex".to_string());

    let block = make_env_block(&env);

    // Verify the block is properly null-terminated
    assert_eq!(block.last(), Some(&0));
    assert_eq!(block[block.len() - 2], 0);  // Double null terminator

    // Verify no embedded nulls in the middle of entries
    let mut i = 0;
    while i < block.len() - 1 {
        if block[i] == 0 {
            // Entry separator: next should be start of new entry or final null
            i += 1;
            continue;
        }
        i += 1;
    }
}

---

3. Proposed Fix

3.1 Fix 1: Safe Environment Block Construction

File: codex-rs/windows-sandbox-rs/src/process.rs

/// Constructs a Windows environment block from a HashMap.
///
/// The block format is: `KEY=VALUE\0KEY2=VALUE2\0\0`
/// Each entry is null-terminated, and the block ends with a double null.
///
/// This implementation is safe against:
/// - Embedded null characters in values (Windows Store paths)
/// - Buffer boundary overruns from wide-string conversion
/// - Double-null corruption from malformed input
pub fn make_env_block(env: &HashMap<String, String>) -> Vec<u16> {
    let mut items: Vec<(String, String)> =
        env.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
    items.sort_by(|a, b| {
        a.0.to_uppercase()
            .cmp(&b.0.to_uppercase())
            .then(a.0.cmp(&b.0))
    });

    // Pre-allocate with capacity estimate to avoid reallocation during construction
    let estimated_size: usize = items
        .iter()
        .map(|(k, v)| k.len() + v.len() + 2) // +2 for '=' and null
        .sum::<usize>()
        + 1; // final double-null
    let mut w: Vec<u16> = Vec::with_capacity(estimated_size);

    for (k, v) in items {
        // Sanitize: strip any embedded null characters from key/value
        let clean_k: String = k.chars().filter(|c| *c != '\0').collect();
        let clean_v: String = v.chars().filter(|c| *c != '\0').collect();

        let entry = format!("{clean_k}={clean_v}");
        let mut wide_entry: Vec<u16> = entry.encode_utf16().collect();
        wide_entry.push(0); // Per-entry null terminator
        w.extend_from_slice(&wide_entry);
    }
    w.push(0); // Block terminator (double null)
    w
}

Key changes:

  • Strip embedded null characters from keys and values before conversion
  • Use encode_utf16() directly instead of to_wide() to avoid platform-specific behavior
  • Pre-allocate buffer to prevent reallocation-related corruption
  • Remove the fragile s.pop() pattern that assumed to_wide() output format

3.2 Fix 2: RAII Wrapper for ACL/DACL Memory

File: codex-rs/windows-sandbox-rs/src/token.rs

/// RAII wrapper for ACL pointers allocated by Windows security APIs.
/// Ensures the ACL is freed exactly once, even on panic or early return.
pub struct LocalAcl {
    acl_ptr: *mut ACL,
}

impl LocalAcl {
    /// Wrap a raw ACL pointer. The caller transfers ownership.
    pub fn new(acl_ptr: *mut ACL) -> Self {
        Self { acl_ptr }
    }

    /// Get the raw pointer without transferring ownership.
    pub fn as_ptr(&self) -> *mut ACL {
        self.acl_ptr
    }

    /// Release ownership without freeing (for successful handoff).
    pub fn into_raw(mut self) -> *mut ACL {
        let ptr = self.acl_ptr;
        self.acl_ptr = std::ptr::null_mut();
        ptr
    }
}

impl Drop for LocalAcl {
    fn drop(&mut self) {
        if !self.acl_ptr.is_null() {
            unsafe {
                LocalFree(self.acl_ptr as HLOCAL);
            }
            self.acl_ptr = std::ptr::null_mut();
        }
    }
}

unsafe impl Send for LocalAcl {}
unsafe impl Sync for LocalAcl {}

Updated set_default_dacl function:

unsafe fn set_default_dacl(h_token: HANDLE, sids: &[*mut c_void]) -> Result<()> {
    if sids.is_empty() {
        return Ok(());
    }

    let entries: Vec<EXPLICIT_ACCESS_W> = sids
        .iter()
        .map(|sid| EXPLICIT_ACCESS_W {
            grfAccessPermissions: GENERIC_ALL,
            grfAccessMode: GRANT_ACCESS,
            grfInheritance: 0,
            Trustee: TRUSTEE_W {
                pMultipleTrustee: std::ptr::null_mut(),
                MultipleTrusteeOperation: 0,
                TrusteeForm: TRUSTEE_IS_SID,
                TrusteeType: TRUSTEE_IS_UNKNOWN,
                ptstrName: *sid as *mut u16,
            },
        })
        .collect();

    let mut p_new_dacl: *mut ACL = std::ptr::null_mut();
    let res = SetEntriesInAclW(
        entries.len() as u32,
        entries.as_ptr(),
        std::ptr::null_mut(),
        &mut p_new_dacl,
    );

    if res != ERROR_SUCCESS {
        return Err(anyhow!("SetEntriesInAclW failed: {res}"));
    }

    // Wrap in RAII guard — automatically freed if SetTokenInformation fails
    let acl_guard = LocalAcl::new(p_new_dacl);

    let mut info = TokenDefaultDaclInfo {
        default_dacl: acl_guard.as_ptr(),
    };

    let ok = SetTokenInformation(
        h_token,
        TokenDefaultDacl,
        &mut info as *mut _ as *mut c_void,
        std::mem::size_of::<TokenDefaultDaclInfo>() as u32,
    );

    if ok == 0 {
        let err = GetLastError();
        // acl_guard drops here, freeing p_new_dacl exactly once
        return Err(anyhow!(
            "SetTokenInformation(TokenDefaultDacl) failed: {err}",
        ));
    }

    // Success: release ownership so the Drop doesn't free it
    // (The ACL is now owned by the token)
    acl_guard.into_raw();
    Ok(())
}

3.3 Fix 3: Safe Process Creation with Buffer Lifetime Management

File: codex-rs/windows-sandbox-rs/src/process.rs

/// # Safety
/// Caller must provide a valid primary token handle (`h_token`) with appropriate access.
pub unsafe fn create_process_as_user(
    h_token: HANDLE,
    argv: &[String],
    cwd: &Path,
    env_map: &HashMap<String, String>,
    logs_base_dir: Option<&Path>,
    stdio: Option<(HANDLE, HANDLE, HANDLE)>,
    use_private_desktop: bool,
) -> Result<CreatedProcess> {
    let cmdline_str = argv_to_command_line(argv);
    let mut cmdline: Vec<u16> = cmdline_str.encode_utf16().chain(std::iter::once(0)).collect();
    let env_block = make_env_block(env_map);
    let desktop = LaunchDesktop::prepare(use_private_desktop, logs_base_dir)?;

    let cwd_wide: Vec<u16> = cwd.to_string_lossy().encode_utf16().chain(std::iter::once(0)).collect();
    let env_block_len = env_block.len();

    // ... (STARTUPINFOEXW setup unchanged) ...

    let creation_flags = CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT;
    let ok = CreateProcessAsUserW(
        h_token,
        std::ptr::null(),
        cmdline.as_mut_ptr(),
        std::ptr::null_mut(),
        std::ptr::null_mut(),
        1,
        creation_flags,
        env_block.as_ptr() as *mut c_void,
        cwd_wide.as_ptr(),
        &si.StartupInfo,
        &mut pi,
    );

    if ok == 0 {
        let err = GetLastError() as i32;
        // Capture error message BEFORE any buffers are dropped
        let msg = format!(
            "CreateProcessAsUserW failed: {} ({}) | cwd=<path_redacted> | cmd_len={} | env_u16_len={}",
            err,
            format_last_error(err),
            cmdline_str.len(),
            env_block_len,
        );
        logging::debug_log(&msg, logs_base_dir);
        // Now buffers are safely dropped — no references remain
        return Err(anyhow!("CreateProcessAsUserW failed: {err}"));
    }

    Ok(CreatedProcess {
        process_info: pi,
        startup_info: si.StartupInfo,
        _desktop: desktop,
    })
}

Key changes:

  • Use encode_utf16().chain([0]) instead of to_wide() for consistent null termination
  • Capture error details into a String (heap-allocated, independent of the input buffers) before any drops
  • Redact sensitive path data from error messages (prevents logging corrupted memory)
  • Remove the cmdline_str dependency from the error path

3.4 Fix 4: Robust Update Check with Atomic File Writes

File: codex-rs/tui/src/updates.rs

async fn check_for_update(version_file: &Path, action: Option<UpdateAction>) -> anyhow::Result<()> {
    let latest_version = match action {
        // ... (existing match arms unchanged) ...
    };

    // Preserve any previously dismissed version if present.
    let prev_info = read_version_info(version_file).ok();
    let info = VersionInfo {
        latest_version,
        last_checked_at: Utc::now(),
        dismissed_version: prev_info.and_then(|p| p.dismissed_version),
    };

    let json_line = format!("{}\n", serde_json::to_string(&info)?);

    // Atomic write: write to temp file, then rename to prevent corruption
    if let Some(parent) = version_file.parent() {
        tokio::fs::create_dir_all(parent).await?;
    }

    // Use a temp file in the same directory to ensure atomic rename
    let temp_file = version_file.with_extension("json.tmp");
    tokio::fs::write(&temp_file, &json_line).await?;
    tokio::fs::rename(&temp_file, version_file).await?;

    Ok(())
}

Additional: Graceful degradation in get_upgrade_version:

pub fn get_upgrade_version(config: &Config) -> Option<String> {
    if !config.check_for_update_on_startup || is_source_build_version(CODEX_CLI_VERSION) {
        return None;
    }

    let action = update_action::get_update_action();
    let version_file = version_filepath(config);

    // Read cached version with robust error handling
    let info = match read_version_info(&version_file) {
        Ok(info) => Some(info),
        Err(e) => {
            // Log but don't crash — corrupted cache is not fatal
            tracing::warn!("Failed to read version cache (non-fatal): {e}");
            None
        }
    };

    if match &info {
        None => true,
        Some(info) => info.last_checked_at < Utc::now() - Duration::hours(20),
    } {
        tokio::spawn(async move {
            check_for_update(&version_file, action)
                .await
                .inspect_err(|e| tracing::error!("Failed to check for update: {e}"))
        });
    }

    info.and_then(|info| {
        if is_newer(&info.latest_version, CODEX_CLI_VERSION).unwrap_or(false) {
            Some(info.latest_version)
        } else {
            None
        }
    })
}

3.5 Fix 5: Windows Store-Specific Update Detection Guard

File: codex-rs/tui/src/updates.rs (new function)

/// Detects if Codex is running from a Windows Store (MSIX) installation.
/// MSIX installs have restricted file system access and cannot use the
/// standard standalone update mechanism.
#[cfg(target_os = "windows")]
fn is_windows_store_install() -> bool {
    if let Ok(exe_path) = std::env::current_exe() {
        let path_str = exe_path.to_string_lossy().to_lowercase();
        // Windows Store apps are installed under WindowsApps
        path_str.contains("windowsapps")
            || path_str.contains("program files\\windows apps")
            || path_str.contains("program files\\windowsapps")
    } else {
        false
    }
}

/// Returns the update action, but returns None for Windows Store installs
/// since they cannot be updated via the standalone installer script.
#[cfg(target_os = "windows")]
pub fn get_update_action() -> Option<UpdateAction> {
    if is_windows_store_install() {
        tracing::info!(
            "Running from Windows Store (MSIX) — \
             updates are managed by the Microsoft Store. \
             Skipping in-app update check."
        );
        return None;
    }
    UpdateAction::from_install_context(InstallContext::current())
}

---

4. Testing Plan

4.1 Unit Tests

// codex-rs/windows-sandbox-rs/src/process.rs
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn make_env_block_empty() {
        let env = HashMap::new();
        let block = make_env_block(&env);
        assert_eq!(block, vec![0, 0]); // Double null for empty block
    }

    #[test]
    fn make_env_block_single_entry() {
        let mut env = HashMap::new();
        env.insert("PATH".to_string(), "C:\\Windows\\System32".to_string());
        let block = make_env_block(&env);
        // Verify structure: "PATH=C:\Windows\System32\0\0"
        assert!(block.ends_with(&[0, 0]));
    }

    #[test]
    fn make_env_block_windows_store_path() {
        let mut env = HashMap::new();
        env.insert(
            "LOCALAPPDATA".to_string(),
            "C:\\Program Files\\WindowsApps\\OpenAI.Codex_1.0.0_x64__abc123".to_string(),
        );
        let block = make_env_block(&env);
        // Verify no embedded nulls in the middle
        let mut null_count = 0;
        for (i, &c) in block.iter().enumerate() {
            if c == 0 {
                null_count += 1;
                // After a null, either another null (end) or start of new entry
                if i + 1 < block.len() - 1 {
                    assert!(block[i + 1] != 0 || i + 1 == block.len() - 1,
                        "Double null in middle of block at index {i}");
                }
            }
        }
        assert_eq!(null_count, 2); // One per-entry null + one block terminator
    }

    #[test]
    fn make_env_block_embedded_null_stripped() {
        let mut env = HashMap::new();
        env.insert("KEY".to_string(), "val\0ue".to_string());
        let block = make_env_block(&env);
        // The embedded null should be stripped
        let decoded: String = block.iter()
            .take_while(|&&c| c != 0)
            .map(|&c| c as u8 as char)
            .collect();
        assert!(!decoded.contains('\0'));
    }

    #[test]
    fn local_acl_double_free_prevention() {
        use windows_sys::Win32::Foundation::LocalAlloc;
        use windows_sys::Win32::System::Memory::LMEM_FIXED;

        unsafe {
            // Allocate a dummy buffer to simulate an ACL
            let ptr = LocalAlloc(LMEM_FIXED, 64) as *mut ACL;
            assert!(!ptr.is_null());

            {
                let guard = LocalAcl::new(ptr);
                // guard goes out of scope here, frees ptr
            }

            // Creating another guard with the same pointer should be safe
            // (the guard checks for null after drop)
            // Note: In real code, you'd never do this — this tests defensive coding
        }
    }
}

4.2 Integration Tests

// codex-rs/tui/src/updates.rs — integration test
#[cfg(test)]
mod integration_tests {
    use super::*;
    use tempfile::TempDir;

    #[tokio::test]
    async fn atomic_write_prevents_corruption() {
        let dir = TempDir::new().unwrap();
        let version_file = dir.path().join("version.json");

        // Write initial valid data
        let info = VersionInfo {
            latest_version: "1.0.0".to_string(),
            last_checked_at: Utc::now(),
            dismissed_version: None,
        };
        let json = format!("{}\n", serde_json::to_string(&info).unwrap());
        tokio::fs::write(&version_file, &json).await.unwrap();

        // Simulate interrupted write (crash during write)
        let temp_file = version_file.with_extension("json.tmp");
        tokio::fs::write(&temp_file, "partial{").await.unwrap();
        // Don't rename — simulate crash

        // Original file should still be valid
        let result = read_version_info(&version_file);
        assert!(result.is_ok());

        // Clean up temp file
        let _ = tokio::fs::remove_file(&temp_file).await;
    }

    #[tokio::test]
    async fn corrupted_version_file_does_not_crash() {
        let dir = TempDir::new().unwrap();
        let version_file = dir.path().join("version.json");

        // Write corrupted data
        tokio::fs::write(&version_file, "not valid json{{{").await.unwrap();

        // read_version_info should return error, not panic
        let result = read_version_info(&version_file);
        assert!(result.is_err());

        // get_upgrade_version should handle this gracefully
        // (would need a full Config mock to test end-to-end)
    }
}

4.3 Manual Testing Matrix

| Test Scenario | Expected Behavior | Pass Criteria |
|---|---|---|
| Normal launch, no update available | TUI starts normally | No crash, no error |
| Launch with update available, download succeeds | Update prompt shown, update proceeds | No crash, update completes |
| Launch with update available, download fails (network) | Graceful error, TUI starts normally | No crash, error logged |
| Launch with corrupted version.json | Cache ignored, TUI starts normally | No crash, warning logged |
| Launch on Windows Store (MSIX) install | Update check skipped, TUI starts | No crash, info logged |
| Launch with read-only version.json | Write fails gracefully, TUI starts | No crash, warning logged |
| Launch with firewall blocking API | Timeout handled, TUI starts | No crash within 30s |
| Multiple rapid launches | No race condition on version.json | No crash, no corruption |

4.4 Stress Testing

# On Windows, run 100 rapid launches with simulated failure
for i in $(seq 1 100); do
    echo "corrupted" > ~/.codex/version.json
    timeout 30 codex --version || true
done
# Verify no crashes occurred

---

5. Impact Assessment

5.1 Severity: P0 — Crash on Launch

This is a P0 (critical) issue because:

  • Zero-workaround: Users cannot launch Codex at all on affected Windows Store installations
  • Broad impact: Any Windows Store user with network issues, firewall rules, or disk permission problems will experience this crash
  • Reproducible: The crash is deterministic given the right conditions (update detected + download fails)
  • Data loss risk: Heap corruption can cause arbitrary memory writes, potentially corrupting user data

5.2 Affected Users

  • Primary: Windows Store (MSIX) installations of Codex
  • Secondary: Standalone Windows installations with network restrictions
  • Estimated affected population: All Windows users with check_for_update_on_startup = true who experience transient network failures during launch

5.3 Risk of Fix

| Risk | Likelihood | Mitigation |
|---|---|---|
| Regressions in environment block handling | Low | Comprehensive unit tests covering edge cases |
| ACL memory management changes | Low | RAII wrapper is strictly safer than manual free |
| Atomic file write on network drives | Low | Fallback to direct write if rename fails |
| Windows Store detection false positives | Low | Detection uses well-known path patterns |
| Performance impact from extra allocations | Negligible | Pre-allocation eliminates reallocation overhead |

5.4 Rollout Plan

  1. Phase 1 (Internal): Merge to main, run full CI test suite including Windows runners
  2. Phase 2 (Canary): Release to Windows Store beta channel, monitor crash reports for 48 hours
  3. Phase 3 (Stable): Roll out to Windows Store stable channel
  4. Monitoring: Track HEAP_CORRUPTION crash rate in Windows Error Reporting

---

6. Files Changed

| File | Change Type | Lines |
|---|---|---|
| codex-rs/windows-sandbox-rs/src/process.rs | Modified | ~40 lines |
| codex-rs/windows-sandbox-rs/src/token.rs | Modified | ~60 lines |
| codex-rs/tui/src/updates.rs | Modified | ~30 lines |
| codex-rs/tui/src/update_action.rs | Modified | ~25 lines |
| codex-rs/windows-sandbox-rs/src/process.rs (tests) | Added | ~80 lines |
| codex-rs/tui/src/updates.rs (tests) | Added | ~60 lines |

---

7. References

---

8. Appendix: Crash Stack Trace (Expected)

ntdll!RtlReportCriticalFailure+0x45
ntdll!RtlpHeapHandleError+0x12
ntdll!RtlpAllocateHeap+0x7a3
ntdll!RtlAllocateHeap+0x1e2
codex!make_env_block+0x8f          // Buffer overrun in environment block
codex!create_process_as_user+0x134  // Use of corrupted heap
codex!spawn_process_with_pipes+0x5a // Windows sandbox spawn
codex!windows_sandbox::setup+0x201  // Sandbox initialization
codex!tui::app::App::new+0x345      // TUI application startup
codex!main+0x178                    // Entry point

The heap corruption originates in make_env_block where the buffer overrun corrupts adjacent heap metadata, then manifests later when the heap manager attempts to allocate or free memory.

Wangnov · 3 months ago

For people using the workaround of manually installing the latest MSIX when Store update/download access is blocked, this unofficial mirror may be useful:

https://github.com/Wangnov/codex-app-mirror

It mirrors the official Codex desktop installers. For Windows, it resolves the current official MSIX from Microsoft Store ProductId 9PLM9XGG6VKS, publishes it as a GitHub Release asset, and includes SHA256SUMS.txt plus release-manifest.json for verification.

Latest Windows MSIX:
https://codexapp.agentsmirror.com/latest/win

This does not fix the underlying crash/update-handling bug, and it is not an official OpenAI distribution channel. It is just a practical way to get the latest official MSIX when Microsoft Store access is restricted.

ronaldwang16 · 2 months ago

I can confirm this is reproducible and environment-dependent, not a one-off — here's a clean side-by-side comparison that might help narrow down the root cause.

Setup:

  • Same Codex Desktop build (26.609.9530.0) installed on two different Windows machines
  • Machine A (work laptop): corporate network policy blocks Microsoft Store entirely (ms-windows-store:// shows "Microsoft Store is blocked, check with your IT or system administrator")
  • Machine B (home PC): no Store restrictions, Get-AppxPackage Microsoft.WindowsStore reports Status: Ok

Result:

┌────────────────────┬───────────────────────────────────────────┬───────────────────────────────────┐
│ │ Machine A (Store blocked) │ Machine B (Store accessible) │
├────────────────────┼───────────────────────────────────────────┼───────────────────────────────────┤
│ Codex Desktop │ 26.609.9530.0 │ 26.609.9530.0 │
│ version │ │

---
I can confirm this is reproducible and environment-dependent, not a one-off — here's a clean side-by-side comparison that might help narrow down the root cause.

Setup:

  • Same Codex Desktop build (26.609.9530.0) installed on two different Windows machines
  • Machine A (work laptop): corporate network policy blocks Microsoft Store entirely (ms-windows-store:// shows "Microsoft Store is blocked, check with your IT or system administrator")
  • Machine B (home PC): no Store restrictions, Get-AppxPackage Microsoft.WindowsStore reports Status: Ok

Result:

┌────────────────────┬───────────────────────────────────────────┬───────────────────────────────────┐
│ │ Machine A (Store blocked) │ Machine B (Store accessible) │
├────────────────────┼───────────────────────────────────────────┼───────────────────────────────────┤
│ Codex Desktop │ 26.609.9530.0 │ 26.609.9530.0 │
│ version │ │ │
├────────────────────┼───────────────────────────────────────────┼───────────────────────────────────┤
│ Launch behavior │ All processes self-terminate within 7–10 │ Launches normally, processes stay │
│ │ seconds │ alive │
└────────────────────┴──────────────────────────────────────────────────┘

Diagnostics on Machine A:

  • Electron log (%LOCALAPPDATA%\Packages\OpenAI.Codex_2p2nqsd0c76g0\LocalCache\Local\Codex\Logs\...\codex-desktop-*.log) consistently stops at the same line: the app detects a newer version available via the Store updater

(manifestBuildVersion=26.611.7849.0) er advances across repeated launches.

  • Ruled out on Machine A: AppLocker, rictions, proxy settings, disk space,WebView2 runtime, non-ASCII path in user profile, sandbox ACL errors.

Conclusion: the failure appears tied specifically to the Store-based update check getting stuck when the Store itself is unreachable, rather t machine config — since the identicalbuild runs fine the moment Store access is available.
Given Codex Desktop's only install/upe Microsoft Store (no standaloneMSIX/sideload installer on the official download page), any environment with Store access policy-blocked — which is common in corporate networks — will likely hit this same crash-on-launch behavior. A fallback that lets the app continue launching instead of silently self-terminating)when the update check fails would resolve this for affected users. CLI install (winget install Codex -s msstore notwithstanding) is the curreonvenience.

Happy to provide logs or test a patched build if that helps.

550466996-ship-it · 1 month ago

Confirmed the same user-visible failure mode on Windows 11 x64, with additional evidence that the Store updater error path can terminate the app even when there is no native crash recorded.

Environment / versions:

  • Installed package before the failure: OpenAI.Codex_26.721.3996.0_x64__2p2nqsd0c76g0
  • Store manifest version detected by the app: 26.721.4979.0

Observed sequence:

  1. Every launch logged [windows-store-updater] Checking Windows Store for package updates with build 26.721.3996.0 and manifest build 26.721.4979.0.
  2. The Microsoft Store catalog request failed with 0x80072EFD / WinHTTP 12029 (ERROR_WINHTTP_CANNOT_CONNECT).
  3. The app opened briefly, then silently exited about 7–9 seconds later. Repeated launches produced the same cycle.
  4. Windows Application Error and WER contained no corresponding Codex crash. AppModel-Runtime only recorded destruction of the AppX container, so this occurrence appears to be an updater-triggered shutdown/exit rather than a conventional native crash.
  5. Manually installing the latest MSIX (26.721.4979.0) restored normal launch, matching the temporary workaround described here.
  6. After Store connectivity was repaired, the exact Codex catalog lookup returned HTTP 200 and the current package launched normally. This strongly isolates Store updater connectivity as the trigger; a Store/network failure should still be non-fatal to the main app.

There is also a related update lifecycle race in AppXDeploymentServer: multiple update attempts staged successfully but registration failed with 0x80073D02 because Codex processes were still running. Registration succeeded only after the app fully exited and Windows retried.

Expected behavior:

  • Store lookup/download/install failures must not terminate or block the main app.
  • Codex should open normally, show an actionable update error, and retry later.
  • Package registration should wait for all Codex processes to exit, then reliably relaunch or clearly tell the user what happened.
  • Users should not be forced into repeatedly downloading and installing the latest MSIX manually after every release.