33 *
44 * On Windows, `chmod` only controls POSIX-style bits in the ACE list and does NOT remove
55 * inherited permissions from other users. Real per-user isolation requires icacls to:
6- * 1. Disable inheritance (icacls path /inheritance:r)
7- * 2. Strip broad explicit grants by SID (Everyone, Users, Authenticated Users)
8- * 3. Grant the current user full control (icacls path /grant:r "CURRENTUSER:(F)")
6+ * 1. Grant the current user full control (icacls path /grant:r "CURRENTUSER:(F)")
7+ * 2. Disable inheritance (icacls path /inheritance:r)
8+ * 3. Strip broad explicit grants by SID (Everyone, Users, Authenticated Users)
9+ *
10+ * Owner grant MUST precede `/inheritance:r`. That flag is destructive: it drops inherited
11+ * ACEs immediately. If a later step times out after inheritance is removed but before an
12+ * explicit owner ACE exists, the temp is left with a protected empty DACL — owned by the
13+ * current user yet unreadable/ununlinkable until Full Control is restored (issue #596).
914 *
1015 * On non-Windows platforms the helpers fall through to the caller's existing chmod-based
1116 * behaviour: they return ok:true without invoking any external process.
@@ -141,8 +146,8 @@ function currentWindowsUser(): string | undefined {
141146
142147/**
143148 * Run icacls to harden a single file system entry.
144- * - Disables inheritance (keeps nothing: /inheritance:r)
145- * - Grants the current user Full Control
149+ * Order is intentional (issue #596): grant the owner ACE first so a later
150+ * `/inheritance:r` or `/remove:g` timeout cannot strand a protected zero-ACE DACL.
146151 *
147152 * We do NOT use a shell string; all arguments are passed as an array so no
148153 * shell injection is possible even for paths with unusual characters.
@@ -170,13 +175,20 @@ function runIcacls(targetPath: string, directory: boolean, deadline: number): vo
170175 if ( ! result . success ) throw icaclsError ( step , result ) ;
171176 } ;
172177
173- // Step 1: disable inheritance and remove inherited ACEs
178+ // Step 1: grant current user full control BEFORE any destructive ACL change.
179+ // If this fails, inheritance is untouched and the writer keeps inherited access.
180+ const grant = directory ? `${ user } :(OI)(CI)(F)` : `${ user } :(F)` ;
181+ runOrThrow ( "/grant:r" , [ targetPath , "/grant:r" , grant ] ) ;
182+
183+ // Step 2: disable inheritance and remove inherited ACEs. The explicit owner ACE
184+ // from step 1 survives this transition, so a later failure still leaves cleanup access.
174185 runOrThrow ( "/inheritance:r" , [ targetPath , "/inheritance:r" ] ) ;
175186
176- // Step 2 : remove broad explicit grants using stable SIDs (not localized names).
187+ // Step 3 : remove broad explicit grants using stable SIDs (not localized names).
177188 // Missing ACEs can yield a non-zero exit; verify with locale-independent /findsid
178189 // before accepting the failure as harmless — a swallowed real failure would leave
179190 // Everyone/Users/Authenticated Users grants while reporting hardened.
191+ // `/remove:g` cannot remove the explicit current-user ACE installed in step 1.
180192 const removal = run ( "/remove:g" , [ targetPath , "/remove:g" , ...BROAD_SIDS ] ) ;
181193 if ( ! removal . success ) {
182194 if ( removal . timedOut ) throw icaclsError ( "/remove:g" , removal ) ;
@@ -191,10 +203,6 @@ function runIcacls(targetPath: string, directory: boolean, deadline: number): vo
191203 }
192204 }
193205 }
194-
195- // Step 3: grant current user full control.
196- const grant = directory ? `${ user } :(OI)(CI)(F)` : `${ user } :(F)` ;
197- runOrThrow ( "/grant:r" , [ targetPath , "/grant:r" , grant ] ) ;
198206}
199207
200208/**
0 commit comments