Skip to content

Fix Windows defects: AppContainer sandbox spawn, token DACL, release-gate scripts - #16

Open
metaphorics wants to merge 8 commits into
devfrom
devin/1791497200-windows-defects
Open

metaphorics wants to merge 8 commits into
devfrom
devin/1791497200-windows-defects

Conversation

@metaphorics

Copy link
Copy Markdown
Contributor

Summary

Fix the Windows defects surfaced by the dal workspace suite on Windows Server 2022 — the AppContainer sandbox timed out and failed spawns, and the release gates could not run.

crates/dalgon/src/sandbox.rs (the bulk of the diff):

  • write_dir_dacl writes a directory's DACL object-only through NtSetSecurityObject. SetNamedSecurityInfoW re-propagates every inheritable ACE on the directory through its whole subtree regardless of the new ACE's flags — measured ~40–50s per call on C:\Users / C:\Users\Administrator, which is what made sandbox_rejects_rm_outside_allowed_roots exceed the gate's timeouts. Traverse grants/revokes now take ~ms.
  • DaclState records orig_label; the first write planter snapshots the object's integrity label and lift restores it on the last holder leaving, so the Low mandatory label does not outlive the run.
  • plant_grants skips PATH directories the container can already read via ALL APPLICATION PACKAGES; otherwise it plants a traverse grant on the directory, the executable, and a bounded (PATH_GRANT_LIMIT = 128) set of runnable files (exe|dll|bat|cmd|ps1|com) from path_grant_files.
  • posix_runtime_import walks the PE import table and refuses executables that import msys-*/cygwin* before spawn; runtime_init_failure maps a 0xC0000142/66 early-init death (e.g. launchers like Git\bin\bash.exe) to the same legible refusal — MSYS/cygwin cannot init inside a classic AppContainer's private \BaseNamedObjects.
  • spawn keeps lpApplicationName NULL: under the container token the loader rejects explicit paths it cannot map, while quoted argv0 resolves through the normal argv path.

crates/dal-wire/src/token.rs — on Windows, token creation stamps a protected DACL D:P(A;;FA;;;<owner>)(A;;FA;;;SY)(A;;FA;;;BA) (the POSIX 0600 counterpart); the crate lint moved forbid(unsafe_code) → deny + #[expect] on mod win.

Repo scripts on Windows — .gitattributes pins *.sh to LF; scripts/*.sh accept python3 or python; release_support/mod.rs resolves bash through %ProgramFiles%\Git\bin\bash.exe before PATH (the WSL stub sorts earlier but cannot open C:\ paths) and normalizes CRLF.

gates/tests/release_guards.rs — expects the release dal first literal that upstream ee66c0a installed in scripts/release.py (the test was stale on dev, failed on every platform).

Verified on this box: cargo fmt --check, cargo clippy --all-targets --all-features --locked -D warnings, and cargo test --workspace --all-features --locked all pass (the long-running shuttle_interleaving_race_proof skipped — see note). dal-full-03 now finishes in ~1.6s.

Not addressed here: open_locked_journal's 60s own-lock retry makes the shuttle test take ~20min on Windows (cross-platform quirk, left as-is).

Link to Devin session: https://app.devin.ai/sessions/8f7410ed0f314371ad3bc98105fb34c9
Open in Devin Desktop: https://app.devin.ai/desktop/session/8f7410ed0f314371ad3bc98105fb34c9?variant=devin
Requested by: @metaphorics

…ontract

- dalgon sandbox: spawn via quoted argv0 (lpApplicationName NULL under
  AppContainer), plant writable-root ACE + Low label via O(1) object
  writes, restore on lift; refuse MSYS/cygwin targets legibly (PE
  import preflight + 0xC0000142 post-exit mapping)
- dal-wire: serve token gets protected owner+SYSTEM+Administrators
  DACL on Windows (0600 parity); crate lint forbid->deny with named
  mod win exception
- gates/release_support: resolve Git Bash before PATH bash (WSL
  launcher cannot open C:\ paths); normalize CRLF streams
- scripts: accept python3 or python; .gitattributes pins *.sh to LF
…se-guard literal

- Traverse grants and revokes on directories write the object DACL alone
  through NtSetSecurityObject: SetNamedSecurityInfoW re-propagates every
  inheritable ACE on the directory through its whole subtree, which cost
  ~40s per call on C:\Users and made the sandbox gate time out.
- record orig_label on first write access and restore it on lift so the
  Low mandatory label does not outlive the run
- skip PATH directories the container can already read through ALL
  APPLICATION PACKAGES; otherwise plant traverse on the directory, the
  executable, and a bounded set of runnable files
- preflight refuses executables importing msys-*/cygwin* and maps the
  0xC0000142/66 early-init death to a legible sandbox refusal
- lpApplicationName stays NULL so argv0 resolves through the normal path
  under the container token
- release_guards: expect the repaired "release dal first" literal from
  upstream ee66c0a
@devin-ai-integration

Copy link
Copy Markdown

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

Original prompt from a

@gosuda/dalgona @gosuda/dalgona-private Fix all defects in Windows.

@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f3e1bf12-dddc-4632-b553-c5534ae2cdf3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-09T03:54:09.629483Z ca7526c Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d1bde28466

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread dal/crates/dalgon/src/sandbox.rs Outdated
Comment on lines +1898 to +1902
if let Some(dll) = posix_runtime_import(Path::new(executable)) {
return Err(format!(
"dalgon sandbox: {} loads {dll}; MSYS/cygwin runtimes cannot run inside the Windows sandbox (AppContainer namespaces are private).",
executable.to_string_lossy()
));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep the default Windows shell runnable

On Windows with sandbox = true and no custom shell, dal-tools/src/exec/shell.rs::default_windows_ladder selects Git's bin\bash.exe, which imports msys-2.0.dll; this new preflight therefore rejects the executable before every default exec call reaches CreateProcessW. The existing Windows gate does not catch this because it only attempts a denied removal and accepts any error containing dalgon sandbox, so the default sandbox configuration now makes all commands fail with exit 126.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The refusal does not introduce the failure — it makes a pre-existing one legible. MSYS/cygwin cannot initialize inside a classic AppContainer: the container gets a private \BaseNamedObjects namespace, so cygwin1/msys-2.0 early init dies with STATUS_DLL_INIT_FAILED (0xC0000142) before main. Before this preflight, the same default-shell exec already failed every call — just with an opaque exit code that read like a denied exec instead of an architecture constraint. Dropping the check would return exit 126/0xC0000142 with no explanation rather than restore a working shell.

The real gap is upstream of this: default_windows_ladder has no usable POSIX shell to offer the Windows sandbox at all. That is a product-level decision (e.g. route default exec through cmd.exe/powershell, or ship a sandboxable shell) rather than something this defect fix can invent here — it is recorded as a decision row in the spec repo so the shell contract gets designed deliberately instead of silently pointing at an executable that cannot run.

Comment thread dal/crates/dalgon/src/sandbox.rs Outdated
Comment thread dal/crates/dalgon/src/sandbox.rs Outdated
Comment thread dal/crates/dalgon/src/sandbox.rs Outdated
Comment thread dal/crates/dalgon/src/sandbox.rs
Comment thread dal/crates/dalgon/src/sandbox.rs Outdated
Comment thread dal/crates/dalgon/src/sandbox.rs Outdated
Comment thread dal/crates/dalgon/src/sandbox.rs Outdated
Comment on lines +1511 to +1513
})
.take(PATH_GRANT_LIMIT)
.collect()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not truncate executable PATH grants arbitrarily

For a PATH directory without a usable ALL APPLICATION PACKAGES grant and more than 128 matching files, read_dir's unspecified enumeration order determines which programs and DLLs receive an ACE. Any command or loader dependency outside that arbitrary prefix remains inaccessible to the AppContainer, so ordinary PATH resolution fails depending on directory layout. Grant the required executable set without silently dropping entries, or resolve and grant each requested program on demand.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Keeping the bound — removing it reintroduces the regression this PR fixes. I tested the unbounded form on this box: PATH dirs that are not packages-readable (Git usr\bin/mingw64\bin carry hundreds of matching files) each need a per-file object-only DACL write at ~30ms, and the first __sandbox plant ran past two minutes — the same "spawn takes tens of seconds" failure mode as the SetNamedSecurityInfoW propagation this PR removes. The cap keeps plant latency bounded and predictable.

On the other half of your suggestion: the invoked executable always gets its own non-optional grant, so the tool the caller actually named can never fall outside the prefix — what the prefix can miss is a co-located DLL or secondary binary past entry 128, an acknowledged limitation. Grant-on-demand for load-time dependencies cannot be done lazily from the parent (the dependency is loaded by the already-running container child, which cannot self-grant), and a propagated directory ACE is exactly the minutes-long subtree write we removed. A directory-level ALL APPLICATION PACKAGES-style grant would be the clean answer but requires the directory to opt into that ACL by design — outside the sandbox's authority.

Comment thread dal/gates/tests/release_support/mod.rs
…ken DACL, bash ladder probes

- capture the write root's label at the first write holder even when read/traverse holders exist, and replay the recorded ACE flags and policy mask on lift instead of hard-coded Low-label defaults (state L record gains flag/policy columns, still reads the old form)
- undo the just-planted grant if the Low label write fails, so a failed launch leaves no live ACE its retired record cannot reap
- run plant_files' per-file DACL edits inside the transact so the DACL lock serializes them against other helpers' read-modify-write
- readable_by_packages now evaluates the ACE mask and skips inherit-only entries, and a denied ACE touching the needed access stops the search
- report only STATUS_DLL_INIT_FAILED as a runtime-init failure; pass other exit codes through without the MSYS diagnosis
- mint the token file's protected DACL in SECURITY_ATTRIBUTES at CreateFileW so the file never exists with a wider inherited DACL
- probe LOCALAPPDATA\Programs\Git in the shell ladders and skip the WSL System32 bash.exe stub during the PATH fallback
read TOKEN_USER via read_unaligned (byte-aligned buffer), u32::try_from for SECURITY_ATTRIBUTES.nLength, rustfmt the touched files
…ippy fixes

- write the writable root's Low label ACE with (OI)(CI) so it reaches the
  whole subtree like the DACL grant does
- hoist the readable_by_packages ACE constants ahead of statements and drop
  the needless borrow in the label-restore rollback
- Git\bin\<tool>.exe launchers carry no msys import but exec the msys
  binary at <root>\usr\bin\<name>; probe that twin so the launcher is
  refused like the binary it forwards to (windows-11-arm ran the launcher
  and died inside the container with a bare exit 5)
- runtime_init_failure now diagnoses any nonzero exit from a binary that
  links an MSYS/cygwin runtime, since the launcher propagates whatever
  code the runtime died with
Get-Acl lives in Microsoft.PowerShell.Security, which fails to autoload
on CI runners without a complete PSModulePath; the raw .NET API needs no
module
@metaphorics

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

let _ = transact(edge, |state| {
if state.holders.get(&key).is_some_and(|h| h.len() == 1) {
state.orig.remove(&key);
}
state.remove_holder(&key, guid, access);

P1 Badge Revoke grants when the post-edit state save fails

If the second transact applies edit_dacl and the Low label successfully but then fails in state.save, this error branch removes the persisted holder without revoking the already-live ACE or restoring the label. A transient state-file write/rename failure can therefore leave a writable root permanently Low and its AppContainer grant undiscoverable by the reaper; roll back the filesystem changes before retiring the holder, or preserve the intent for recovery.

AGENTS.md reference: AGENTS.md:L107-L109

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1577 to +1581
let _ = transact(edge, |state| {
for path in recorded.iter().skip(planted.len()) {
let key = path_key(path);
if state.holders.get(&key).is_some_and(|h| h.len() == 1) {
state.orig.remove(&key);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Lift successful batch grants before returning an error

When edit_dacl fails after one or more earlier files in the batch succeeded, this cleanup removes intents only for the failed and unattempted suffix. The successfully edited paths remain in planted, but returning Err discards that list, so plant_grants cannot include them in lift_all; their ACEs and holder records remain after this helper exits until some later sandbox transaction happens to reap them. Revoke the successful prefix here or return it to the caller for cleanup.

AGENTS.md reference: AGENTS.md:L129-L131

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed in 98318d5. The error branch now revokes each planted path's ACE and retires its holder (only once the revoke succeeds — a miss keeps the holder for the reaper to retry), then retires the never-attempted suffix as before. Nothing stays live but untracked when the helper returns Err.

- plant: if edit_dacl + the Low label landed but state.save failed, the
  error path retired the holder while the ACE stayed live and the root
  stayed Low — undo revoke + label restore before dropping the record,
  and keep the holder when the revoke misses so the reaper can retry
- plant_files: returning Err dropped the planted prefix, leaving those
  ACEs and holders alive until a later reap; revoke each planted path
  and retire its holder alongside the never-attempted suffix
@devin-ai-integration

Copy link
Copy Markdown

Re the P1 "revoke grants when the post-edit state save fails": fixed in 98318d5 — the retire transact now revokes the ACE (and restores the original label for write grants) before dropping the holder record; a missed revoke keeps the holder so the reaper can retry the lift.

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