Windows elevated sandbox: native spellchecker creates garbled relative directories, then returns E_ACCESSDENIED
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
What version of the Codex App are you using (From “About Codex” dialog)?
Codex desktop for Windows. The copied sandbox command runner is named codex-command-runner-0.151.0-alpha.7.1.exe; this is the runner version identifier, not a verified desktop About-dialog version.
What subscription do you have?
Not included; the reproduction is a local native Windows sandbox operation with no model/API calls.
What platform is your computer?
- Windows x64, build 26200.9168, display version 25H2.
- Windows sandbox mode:
elevated; workspace-write; child process runs under the dedicatedCodexSandboxOfflineaccount. - COM server:
%SystemRoot%\System32\MsSpellCheckingFacility.dll, version 10.0.26100.8875, valid Microsoft Windows Authenticode signature. - Python 3.14, standard library
ctypesonly. - Observed and reproduced on 2026-08-31.
What issue are you seeing?
Processes launched by Codex's Windows sandbox repeatedly create stray directories with short garbled Unicode names in their working directory. These were initially noticed during automated Electron launches, but Electron and application code are not required: the Windows spelling COM API alone reproduces the problem.
ISpellCheckerFactory::CreateSpellChecker(L"en-US", ...) creates a malformed relative directory with descendants Microsoft\Spelling\neutral, then returns 0x80070005 (E_ACCESSDENIED) with a null spellchecker. The created subtree contains no files. Root names vary between runs; one uninstrumented run produced the JSON-escaped name "\u0d78\ucdf8\u0251".
COM initialization, factory creation, supported-language enumeration, and IsSupported(L"en-US") all succeed before that call, with no relative directories created. The normal-account control succeeds and leaves its disposable working directory empty.
| Same self-contained script | CreateSpellChecker | Checker returned | Relative entries |
|---|---|---|---|
| Codex elevated Windows sandbox | 0x80070005 |
No | Garbled root and empty spelling subtree |
| Normal Windows account, outside sandbox | 0x00000000 |
Yes | None |
The repeated stray folders clutter the project root whenever a launched process inherits it as its working directory.
What steps can reproduce the bug?
- Save the script below as
public-spelling-repro.pyin a writable disposable folder within a Codex Windows workspace. - Have Codex execute
python public-spelling-repro.py sandbox-caseusing its elevated Windows sandbox. The script creates a fresh child directory beside itself and uses that as its working directory, containing the stray writes. - Observe that all earlier stages return success;
create-checkerreturns0x80070005,created: false, and a garbled entry. Inspect that entry to see the emptyMicrosoft\Spelling\neutralsubtree. - As a control, run the same script from an ordinary Windows terminal as
python public-spelling-repro.py normal-case. On the affected machine this returns success withcreated: trueandentries: [].
Use fresh case names for each run. This script deliberately retains its disposable directories for inspection. It prints only status codes and relative directory names, not personal known-folder paths. The exact script below was tested in both contexts. COM declarations were checked against the Windows SDK's spellcheck.h.
Self-contained Python reproduction (no Electron, third-party dependencies, or instrumentation)
"""Windows-only native spelling reproduction; Python standard library only.
Usage: python public-spelling-repro.py fresh-case-name
Creates one disposable directory beside this script; does not delete evidence.
"""
import ctypes as c
from ctypes import wintypes as w
import json
import os
from pathlib import Path
import sys
import uuid
class GUID(c.Structure):
_fields_ = [("data", c.c_ubyte * 16)]
def guid(value):
return GUID.from_buffer_copy(uuid.UUID(value).bytes_le)
def method(pointer, index, result_type, *argument_types):
vtable = c.cast(pointer, c.POINTER(c.POINTER(c.c_void_p))).contents
return c.WINFUNCTYPE(result_type, c.c_void_p, *argument_types)(vtable[index])
def release(pointer):
if pointer.value:
method(pointer, 2, w.ULONG)(pointer)
def record(stage, result, **values):
print(json.dumps(dict(stage=stage, hresult=f"0x{result & 0xffffffff:08x}",
entries=os.listdir("."), **values), ensure_ascii=True),
flush=True)
if len(sys.argv) != 2 or Path(sys.argv[1]).name != sys.argv[1] or sys.argv[1] in (".", ".."):
raise SystemExit("Provide a fresh directory name, not a path")
case = Path(__file__).resolve().parent / sys.argv[1]
case.mkdir()
os.chdir(case)
ole = c.WinDLL("ole32")
shell = c.WinDLL("shell32")
ole.CoInitializeEx.argtypes = [c.c_void_p, w.DWORD]
ole.CoInitializeEx.restype = c.c_long
ole.CoUninitialize.argtypes = []
ole.CoUninitialize.restype = None
ole.CoCreateInstance.argtypes = [c.POINTER(GUID), c.c_void_p, w.DWORD,
c.POINTER(GUID), c.POINTER(c.c_void_p)]
ole.CoCreateInstance.restype = c.c_long
ole.CoTaskMemFree.argtypes = [c.c_void_p]
ole.CoTaskMemFree.restype = None
shell.SHGetKnownFolderPath.argtypes = [c.POINTER(GUID), w.DWORD, c.c_void_p,
c.POINTER(c.c_void_p)]
shell.SHGetKnownFolderPath.restype = c.c_long
for folder, folder_id in [
("roaming", "3EB685DB-65F9-4CF6-A03A-E3EF65729F3D"),
("local", "F1B32785-6FBA-4FCF-9D55-7B8E7F157091"),
]:
for flags in (0, 0x4000):
output = c.c_void_p()
hr = shell.SHGetKnownFolderPath(c.byref(guid(folder_id)), flags, None,
c.byref(output))
record("known-folder", hr, folder=folder, flags=flags)
if output.value:
ole.CoTaskMemFree(output)
hr = ole.CoInitializeEx(None, 2) # COINIT_APARTMENTTHREADED
record("com-initialize", hr)
if hr < 0:
raise SystemExit("COM initialization failed")
factory = c.c_void_p()
enumeration = c.c_void_p()
checker = c.c_void_p()
try:
hr = ole.CoCreateInstance(
c.byref(guid("7AB36653-1796-484B-BDFA-E74F1DB7C1DC")), None, 1,
c.byref(guid("8E018A9D-2415-4677-BF08-794EA61F94BB")), c.byref(factory))
record("create-factory", hr)
if hr < 0 or not factory.value:
raise SystemExit("Factory creation failed")
hr = method(factory, 3, c.c_long, c.POINTER(c.c_void_p))(
factory, c.byref(enumeration))
record("supported-languages", hr)
release(enumeration)
enumeration = c.c_void_p()
supported = w.BOOL()
hr = method(factory, 4, c.c_long, w.LPCWSTR, c.POINTER(w.BOOL))(
factory, "en-US", c.byref(supported))
record("is-supported", hr, supported=bool(supported))
hr = method(factory, 5, c.c_long, w.LPCWSTR, c.POINTER(c.c_void_p))(
factory, "en-US", c.byref(checker))
record("create-checker", hr, created=bool(checker.value))
finally:
release(checker)
release(enumeration)
release(factory)
ole.CoUninitialize()
Relevant output from the sandbox run:
{"stage":"com-initialize","hresult":"0x00000000","entries":[]}
{"stage":"create-factory","hresult":"0x00000000","entries":[]}
{"stage":"supported-languages","hresult":"0x00000000","entries":[]}
{"stage":"is-supported","hresult":"0x00000000","entries":[],"supported":true}
{"stage":"create-checker","hresult":"0x80070005","entries":["\u0d78\ucdf8\u0251"],"created":false}
The same control's final line:
{"stage":"create-checker","hresult":"0x00000000","entries":[],"created":true}
What is the expected behavior?
Native spelling initialization should either succeed using a valid isolated profile, or fail cleanly if a required operation is denied. It should not attempt filesystem writes through malformed relative paths.
A fix should preserve the sandbox's registry and filesystem boundaries. Granting unrestricted access to the real user's registry or disabling the sandbox is not a proposed repair.
Additional information
Further isolation already performed:
- Roaming/local
SHGetKnownFolderPathcalls succeed before the failure. A separate experiment with a correctly formed privateUSERPROFILE\AppData\RoamingandAppData\Local, redirecting only the probe's environment, also reproduced the failure even though known-folder lookups resolved to those private paths. - An optional native import trace in a separate disposable probe process observed the spelling DLL receiving already-malformed relative paths in
CreateFileWandCreateDirectoryW.CreateFileWfor...\default.dicreturnedERROR_PATH_NOT_FOUND; directory creation for the garbled root and spelling descendants succeeded. - Later in the same native call,
RegCreateKeyExWforSoftware\Microsoft\Spelling\Dictionaries, requesting0x2(KEY_SET_VALUE), returned 5 (ERROR_ACCESS_DENIED). The enclosingCreateSpellCheckerreturnedE_ACCESSDENIED. - That trace does not identify where the path first becomes malformed and does not prove that the later registry denial caused the earlier corruption. The origin of the malformed string and the precise Windows/Codex responsibility remain open.
- The optional trace changed only import slots in its own disposable process, forwarded original arguments/results, preserved last-error state, and restored the slots. No installed binaries, ACLs, registry permissions, or sandbox configuration were modified. The included uninstrumented reproduction establishes that tracing is unnecessary for the defect.
- Giving automated Electron launches an absolute disposable profile directory as their working directory contains the unwanted relative writes. That is containment only, not a native fix. Disabling Electron spellcheck alone did not prevent the original symptom.
This report intentionally excludes private application source, personal paths, machine identifiers, full session logs, dictionary contents, and sandbox secrets. Searches for the native DLL name, spelling subtree, and garbled/Chinese-folder symptom did not identify a matching existing report.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the self-contained public-spelling-repro.py and compare its elevated sandbox and normal-account results at ISpellCheckerFactory::CreateSpellChecker. Investigate where the malformed relative paths originate and how the later registry denial relates to E_ACCESSDENIED, while preserving the sandbox boundaries. Done means the native initialization no longer creates malformed relative directories and either succeeds or fails cleanly.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, rust
- Domain
- operating-systems, security
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100