javi-forge 1.30.1 → 1.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/claude-hooks/javi-forge-windows-secure-object.ps1 +1223 -0
- package/assets/claude-hooks/manifest.json +1 -1
- package/dist/lib/__fixtures__/fake-helper-transport.d.ts +46 -0
- package/dist/lib/__fixtures__/fake-helper-transport.js +90 -0
- package/dist/lib/__fixtures__/fake-secure-fs.d.ts +12 -0
- package/dist/lib/__fixtures__/fake-secure-fs.js +26 -1
- package/dist/lib/secure-fs-posix.d.ts +13 -4
- package/dist/lib/secure-fs-posix.js +33 -5
- package/dist/lib/secure-fs-transaction.d.ts +29 -1
- package/dist/lib/secure-fs-transaction.js +65 -5
- package/dist/lib/secure-fs-windows.d.ts +124 -0
- package/dist/lib/secure-fs-windows.js +588 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1223 @@
|
|
|
1
|
+
# javi-forge Windows secure-object helper (SkillGuard Slice 3b, Phase 3).
|
|
2
|
+
#
|
|
3
|
+
# This is the ONLY place a Windows security decision is made. It is a long-lived,
|
|
4
|
+
# framed-stdin session process spawned by src/lib/secure-fs-windows.ts
|
|
5
|
+
# (createPs1Session) after a sha256 digest match against manifest.json. It owns
|
|
6
|
+
# the real OS handles (a handleId -> SafeHandle table for the parent-chain lock)
|
|
7
|
+
# and computes Predicate A (lenient runtime gate) / Predicate B (strict creation)
|
|
8
|
+
# / proveManagedContainer (CREATE_PARENT_DIR) verdicts on FRESH no-follow kernel
|
|
9
|
+
# handles per call.
|
|
10
|
+
#
|
|
11
|
+
# CANNOT be run or validated on the Linux dev box. Correctness rests on:
|
|
12
|
+
# (a) faithful adherence to design.md (Decisions 1/1a/1b/2/3, Predicate A/B),
|
|
13
|
+
# (b) EXACT protocol match with secure-fs-windows.ts (HelperOp/HelperRequest/
|
|
14
|
+
# HelperResponse shapes; the framed [uint32 BE length][UTF-8 JSON] wire),
|
|
15
|
+
# (c) Windows PowerShell 5.1 syntax discipline + PURE ASCII.
|
|
16
|
+
# The Phase 5 windows-latest CI job is the ONLY validator (design Decision 3).
|
|
17
|
+
#
|
|
18
|
+
# Grounding-probe lessons applied (scripts/win-acl-probe.ps1 ran on windows-latest):
|
|
19
|
+
# - Pure ASCII only: a stray em-dash broke PS 5.1 parsing. No non-ASCII bytes.
|
|
20
|
+
# - Enum bit-tests via [int] casts: a bare enum -band threw InvalidCastException.
|
|
21
|
+
# All flag/mask math below is done in C# on int/uint, cast explicitly.
|
|
22
|
+
#
|
|
23
|
+
# Protocol (secure-fs-windows.ts):
|
|
24
|
+
# Wire unit : [uint32 big-endian byteLength][UTF-8 JSON body], both directions.
|
|
25
|
+
# Handshake : first frame emitted = {"ready":true,"protocolVersion":1}.
|
|
26
|
+
# Serial : exactly one outstanding request; response before the next request.
|
|
27
|
+
# Bounded : reject any declared length > HELPER_FRAME_LIMIT (8 MiB).
|
|
28
|
+
# stdout : ONLY length-prefixed frames. All diagnostics go to stderr.
|
|
29
|
+
# ops : openDir revalidate proveOwner proveDacl proveContainer createDir
|
|
30
|
+
# capture writeExcl applyMode rename unlink rmdir releaseHandle.
|
|
31
|
+
|
|
32
|
+
$ErrorActionPreference = 'Stop'
|
|
33
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
34
|
+
|
|
35
|
+
$script:FRAME_LIMIT = 8 * 1024 * 1024
|
|
36
|
+
$script:PROTOCOL_VERSION = 1
|
|
37
|
+
|
|
38
|
+
# --- native + predicate core (C#: int/uint mask math, no PS enum -band) -------
|
|
39
|
+
|
|
40
|
+
$csharp = @'
|
|
41
|
+
using System;
|
|
42
|
+
using System.IO;
|
|
43
|
+
using System.Collections.Generic;
|
|
44
|
+
using System.Runtime.InteropServices;
|
|
45
|
+
using System.Security.AccessControl;
|
|
46
|
+
using System.Security.Principal;
|
|
47
|
+
using Microsoft.Win32.SafeHandles;
|
|
48
|
+
|
|
49
|
+
namespace JaviForge
|
|
50
|
+
{
|
|
51
|
+
public class OpResult
|
|
52
|
+
{
|
|
53
|
+
public bool Ok;
|
|
54
|
+
public string Refusal;
|
|
55
|
+
public string Detail;
|
|
56
|
+
public int Status; // win32 error code; drives the notFound mapping on openDir
|
|
57
|
+
public string HandleId; // openDir / createDir
|
|
58
|
+
public string Opaque; // openDir / createDir / capture
|
|
59
|
+
public int Attributes; // openDir / createDir (dwFileAttributes)
|
|
60
|
+
public string BytesB64; // capture
|
|
61
|
+
|
|
62
|
+
public static OpResult Good() { OpResult r = new OpResult(); r.Ok = true; return r; }
|
|
63
|
+
public static OpResult Fail(string refusal, string detail)
|
|
64
|
+
{
|
|
65
|
+
OpResult r = new OpResult();
|
|
66
|
+
r.Ok = false; r.Refusal = refusal; r.Detail = detail; return r;
|
|
67
|
+
}
|
|
68
|
+
public static OpResult FailStatus(string refusal, string detail, int status)
|
|
69
|
+
{
|
|
70
|
+
OpResult r = Fail(refusal, detail); r.Status = status; return r;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
internal class Held
|
|
75
|
+
{
|
|
76
|
+
public IntPtr Handle;
|
|
77
|
+
public string Path;
|
|
78
|
+
public string Opaque;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public static class SecureObj
|
|
82
|
+
{
|
|
83
|
+
// --- refusal classes (must match SecureRefusal in secure-fs-transaction.ts) -
|
|
84
|
+
private const string R_DACL = "unsafe-windows-dacl";
|
|
85
|
+
private const string R_CHAIN = "unsafe-parent-chain";
|
|
86
|
+
|
|
87
|
+
// --- access-right / attribute constants ---------------------------------
|
|
88
|
+
private const uint GENERIC_READ = 0x80000000;
|
|
89
|
+
private const uint GENERIC_WRITE = 0x40000000;
|
|
90
|
+
private const uint READ_CONTROL = 0x00020000;
|
|
91
|
+
private const uint WRITE_DAC_A = 0x00040000;
|
|
92
|
+
private const uint WRITE_OWNER_A = 0x00080000;
|
|
93
|
+
private const uint DELETE_A = 0x00010000;
|
|
94
|
+
private const uint FILE_READ_ATTRIBUTES = 0x0080;
|
|
95
|
+
|
|
96
|
+
private const uint FILE_SHARE_READ = 0x1;
|
|
97
|
+
private const uint FILE_SHARE_WRITE = 0x2;
|
|
98
|
+
private const uint FILE_SHARE_DELETE = 0x4;
|
|
99
|
+
|
|
100
|
+
private const uint OPEN_EXISTING = 3;
|
|
101
|
+
private const uint CREATE_NEW = 1;
|
|
102
|
+
|
|
103
|
+
private const uint FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000;
|
|
104
|
+
private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
|
|
105
|
+
private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
|
|
106
|
+
|
|
107
|
+
private const uint FILE_ATTRIBUTE_DIRECTORY = 0x10;
|
|
108
|
+
private const uint FILE_ATTRIBUTE_REPARSE_POINT = 0x400;
|
|
109
|
+
|
|
110
|
+
private const int ERROR_FILE_NOT_FOUND = 2;
|
|
111
|
+
private const int ERROR_PATH_NOT_FOUND = 3;
|
|
112
|
+
private const int ERROR_FILE_EXISTS = 80;
|
|
113
|
+
private const int ERROR_ALREADY_EXISTS = 183;
|
|
114
|
+
private const int ERROR_DIR_NOT_EMPTY = 145;
|
|
115
|
+
|
|
116
|
+
private const uint MOVEFILE_REPLACE_EXISTING = 0x1;
|
|
117
|
+
private const uint MOVEFILE_WRITE_THROUGH = 0x8;
|
|
118
|
+
|
|
119
|
+
// GetSecurityInfo / SetKernelObjectSecurity information classes
|
|
120
|
+
private const uint OWNER_SECURITY_INFORMATION = 0x1;
|
|
121
|
+
private const uint DACL_SECURITY_INFORMATION = 0x4;
|
|
122
|
+
private const uint PROTECTED_DACL_SECURITY_INFORMATION = 0x80000000;
|
|
123
|
+
private const int SE_FILE_OBJECT = 1;
|
|
124
|
+
|
|
125
|
+
// FILE_INFO_BY_HANDLE_CLASS.FileDispositionInfo
|
|
126
|
+
private const int FileDispositionInfo = 4;
|
|
127
|
+
|
|
128
|
+
// --- Predicate A / B / container path-endangering masks (design.md) ------
|
|
129
|
+
// FILE_WRITE_DATA / FILE_ADD_FILE = 0x0002
|
|
130
|
+
// FILE_APPEND_DATA / FILE_ADD_SUBDIRECTORY = 0x0004
|
|
131
|
+
// FILE_DELETE_CHILD = 0x0040
|
|
132
|
+
// DELETE = 0x00010000 WRITE_DAC = 0x00040000 WRITE_OWNER = 0x00080000
|
|
133
|
+
private const int ADD_FILE = 0x0002;
|
|
134
|
+
private const int ADD_SUBDIR = 0x0004;
|
|
135
|
+
private const int DELETE_CHILD = 0x0040;
|
|
136
|
+
// File-object write bits (same numeric values as ADD_FILE/ADD_SUBDIR on a
|
|
137
|
+
// container, but named for the file mask so PATH_ENDANGER_FILE is self-describing).
|
|
138
|
+
private const int FILE_WRITE_DATA = 0x0002;
|
|
139
|
+
private const int FILE_APPEND_DATA = 0x0004;
|
|
140
|
+
private const int MASK_DELETE = 0x00010000;
|
|
141
|
+
private const int MASK_WRITE_DAC = 0x00040000;
|
|
142
|
+
private const int MASK_WRITE_OWNER = 0x00080000;
|
|
143
|
+
private const int PATH_ENDANGER_COMMON = MASK_DELETE | MASK_WRITE_DAC | MASK_WRITE_OWNER;
|
|
144
|
+
private const int PATH_ENDANGER_DIR = PATH_ENDANGER_COMMON | DELETE_CHILD;
|
|
145
|
+
private const int PATH_ENDANGER_FILE = PATH_ENDANGER_COMMON | FILE_WRITE_DATA | FILE_APPEND_DATA;
|
|
146
|
+
private const int CREATE_PARENT_DIR = PATH_ENDANGER_DIR | ADD_FILE | ADD_SUBDIR;
|
|
147
|
+
|
|
148
|
+
private const int FILE_ALL_ACCESS = 0x001F01FF;
|
|
149
|
+
|
|
150
|
+
// NT SERVICE\TrustedInstaller (trusted OWNER only; JDB-204 / F4a).
|
|
151
|
+
private const string SID_TRUSTED_INSTALLER =
|
|
152
|
+
"S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464";
|
|
153
|
+
|
|
154
|
+
private static readonly object Gate = new object();
|
|
155
|
+
private static readonly Dictionary<string, Held> Handles = new Dictionary<string, Held>();
|
|
156
|
+
private static long HandleSeq = 0;
|
|
157
|
+
|
|
158
|
+
// --- P/Invoke -----------------------------------------------------------
|
|
159
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
160
|
+
private static extern IntPtr CreateFileW(string lpFileName, uint dwDesiredAccess,
|
|
161
|
+
uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition,
|
|
162
|
+
uint dwFlagsAndAttributes, IntPtr hTemplateFile);
|
|
163
|
+
|
|
164
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
165
|
+
private static extern bool CreateDirectoryW(string lpPathName, IntPtr lpSecurityAttributes);
|
|
166
|
+
|
|
167
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
168
|
+
private static extern bool MoveFileExW(string from, string to, uint flags);
|
|
169
|
+
|
|
170
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
171
|
+
private static extern bool RemoveDirectoryW(string lpPathName);
|
|
172
|
+
|
|
173
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
174
|
+
private static extern IntPtr FindFirstFileW(string lpFileName,
|
|
175
|
+
out WIN32_FIND_DATA lpFindFileData);
|
|
176
|
+
|
|
177
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
178
|
+
private static extern bool FindNextFileW(IntPtr hFindFile,
|
|
179
|
+
out WIN32_FIND_DATA lpFindFileData);
|
|
180
|
+
|
|
181
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
182
|
+
private static extern bool FindClose(IntPtr hFindFile);
|
|
183
|
+
|
|
184
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
185
|
+
private static extern bool CloseHandle(IntPtr hObject);
|
|
186
|
+
|
|
187
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
188
|
+
private static extern bool FlushFileBuffers(IntPtr hFile);
|
|
189
|
+
|
|
190
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
191
|
+
private static extern bool GetFileInformationByHandle(IntPtr hFile,
|
|
192
|
+
out BY_HANDLE_FILE_INFORMATION lpFileInformation);
|
|
193
|
+
|
|
194
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
195
|
+
private static extern bool SetFileInformationByHandle(IntPtr hFile, int cls,
|
|
196
|
+
ref FILE_DISPOSITION_INFO info, int dwBufferSize);
|
|
197
|
+
|
|
198
|
+
[DllImport("advapi32.dll", SetLastError = true)]
|
|
199
|
+
private static extern uint GetSecurityInfo(IntPtr handle, int ObjectType,
|
|
200
|
+
uint SecurityInfo, out IntPtr ppsidOwner, out IntPtr ppsidGroup,
|
|
201
|
+
out IntPtr ppDacl, out IntPtr ppSacl, out IntPtr ppSecurityDescriptor);
|
|
202
|
+
|
|
203
|
+
[DllImport("advapi32.dll", SetLastError = true)]
|
|
204
|
+
private static extern bool SetKernelObjectSecurity(IntPtr Handle,
|
|
205
|
+
uint SecurityInformation, byte[] SecurityDescriptor);
|
|
206
|
+
|
|
207
|
+
[DllImport("advapi32.dll", SetLastError = true)]
|
|
208
|
+
private static extern uint GetSecurityDescriptorLength(IntPtr pSecurityDescriptor);
|
|
209
|
+
|
|
210
|
+
[DllImport("advapi32.dll", SetLastError = true)]
|
|
211
|
+
private static extern void MapGenericMask(ref uint AccessMask, ref GENERIC_MAPPING map);
|
|
212
|
+
|
|
213
|
+
[DllImport("kernel32.dll")]
|
|
214
|
+
private static extern IntPtr LocalFree(IntPtr hMem);
|
|
215
|
+
|
|
216
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
217
|
+
private struct FILETIME_S { public uint Low; public uint High; }
|
|
218
|
+
|
|
219
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
220
|
+
private struct BY_HANDLE_FILE_INFORMATION
|
|
221
|
+
{
|
|
222
|
+
public uint dwFileAttributes;
|
|
223
|
+
public FILETIME_S ftCreationTime;
|
|
224
|
+
public FILETIME_S ftLastAccessTime;
|
|
225
|
+
public FILETIME_S ftLastWriteTime;
|
|
226
|
+
public uint dwVolumeSerialNumber;
|
|
227
|
+
public uint nFileSizeHigh;
|
|
228
|
+
public uint nFileSizeLow;
|
|
229
|
+
public uint nNumberOfLinks;
|
|
230
|
+
public uint nFileIndexHigh;
|
|
231
|
+
public uint nFileIndexLow;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
235
|
+
private struct GENERIC_MAPPING
|
|
236
|
+
{
|
|
237
|
+
public uint GenericRead;
|
|
238
|
+
public uint GenericWrite;
|
|
239
|
+
public uint GenericExecute;
|
|
240
|
+
public uint GenericAll;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
244
|
+
private struct SECURITY_ATTRIBUTES
|
|
245
|
+
{
|
|
246
|
+
public int nLength;
|
|
247
|
+
public IntPtr lpSecurityDescriptor;
|
|
248
|
+
public int bInheritHandle;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
252
|
+
private struct FILE_DISPOSITION_INFO { public int DeleteFile; }
|
|
253
|
+
|
|
254
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
255
|
+
private struct WIN32_FIND_DATA
|
|
256
|
+
{
|
|
257
|
+
public uint dwFileAttributes;
|
|
258
|
+
public FILETIME_S ftCreationTime;
|
|
259
|
+
public FILETIME_S ftLastAccessTime;
|
|
260
|
+
public FILETIME_S ftLastWriteTime;
|
|
261
|
+
public uint nFileSizeHigh;
|
|
262
|
+
public uint nFileSizeLow;
|
|
263
|
+
public uint dwReserved0;
|
|
264
|
+
public uint dwReserved1;
|
|
265
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
|
|
266
|
+
public string cFileName;
|
|
267
|
+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
|
|
268
|
+
public string cAlternateFileName;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private static GENERIC_MAPPING FileMapping()
|
|
272
|
+
{
|
|
273
|
+
// Standard file object generic mapping (used by MapGenericMask).
|
|
274
|
+
GENERIC_MAPPING m = new GENERIC_MAPPING();
|
|
275
|
+
m.GenericRead = 0x00120089; // FILE_GENERIC_READ
|
|
276
|
+
m.GenericWrite = 0x00120116; // FILE_GENERIC_WRITE
|
|
277
|
+
m.GenericExecute = 0x001200A0;// FILE_GENERIC_EXECUTE
|
|
278
|
+
m.GenericAll = 0x001F01FF; // FILE_ALL_ACCESS
|
|
279
|
+
return m;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private static readonly IntPtr INVALID_HANDLE = new IntPtr(-1);
|
|
283
|
+
|
|
284
|
+
// --- trusted-principal sets (Predicate A rule 1 / rule 2) ---------------
|
|
285
|
+
private static string CurrentUserSid()
|
|
286
|
+
{
|
|
287
|
+
WindowsIdentity id = WindowsIdentity.GetCurrent();
|
|
288
|
+
return id.User.Value;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private static string SystemSid()
|
|
292
|
+
{
|
|
293
|
+
return new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null).Value;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private static string AdminsSid()
|
|
297
|
+
{
|
|
298
|
+
return new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).Value;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// CREATOR OWNER (S-1-3-0) is treated as owner-equivalent by design: this is
|
|
302
|
+
// the ratified round-4 JDB-202 handling, NOT an accidental widening.
|
|
303
|
+
// Materialized non-inherit-only CREATOR OWNER ACEs are not normally
|
|
304
|
+
// attacker-grantable, and inherit-only ones are already skipped (rule 4).
|
|
305
|
+
private static string CreatorOwnerSid()
|
|
306
|
+
{
|
|
307
|
+
return new SecurityIdentifier(WellKnownSidType.CreatorOwnerSid, null).Value;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Owner is trusted (rule 1): current-user, SYSTEM, Administrators,
|
|
311
|
+
// TrustedInstaller (owner-only), or a materialized CREATOR OWNER
|
|
312
|
+
// (owner-equivalent).
|
|
313
|
+
private static bool OwnerTrusted(string sid)
|
|
314
|
+
{
|
|
315
|
+
if (sid == null) return false;
|
|
316
|
+
return sid == CurrentUserSid() || sid == SystemSid() || sid == AdminsSid()
|
|
317
|
+
|| sid == SID_TRUSTED_INSTALLER || sid == CreatorOwnerSid();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// A trustee is foreign (rule 2) unless it is current-user, SYSTEM,
|
|
321
|
+
// Administrators, or a materialized CREATOR OWNER (owner-equivalent).
|
|
322
|
+
// TrustedInstaller is NOT in the trustee allowlist (owner-only).
|
|
323
|
+
private static bool IsForeignTrustee(string sid)
|
|
324
|
+
{
|
|
325
|
+
if (sid == null) return true;
|
|
326
|
+
if (sid == CurrentUserSid() || sid == SystemSid() || sid == AdminsSid()
|
|
327
|
+
|| sid == CreatorOwnerSid()) return false;
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// --- opaque identity ----------------------------------------------------
|
|
332
|
+
// "<volumeSerialHex>:<fileIdHex>", lowercase; zero FileId is rejected by
|
|
333
|
+
// validOpaque in secure-fs-windows.ts, and refused here at capture too (C4).
|
|
334
|
+
private static string BuildOpaque(BY_HANDLE_FILE_INFORMATION info, out bool zero)
|
|
335
|
+
{
|
|
336
|
+
ulong fileId = ((ulong)info.nFileIndexHigh << 32) | (ulong)info.nFileIndexLow;
|
|
337
|
+
zero = (fileId == 0);
|
|
338
|
+
return info.dwVolumeSerialNumber.ToString("x") + ":" + fileId.ToString("x");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// --- Predicate B: self-relative, protected, owner-only allowlist DACL ---
|
|
342
|
+
private static byte[] BuildProtectedSd()
|
|
343
|
+
{
|
|
344
|
+
SecurityIdentifier user = WindowsIdentity.GetCurrent().User;
|
|
345
|
+
SecurityIdentifier system = new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null);
|
|
346
|
+
SecurityIdentifier admins = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null);
|
|
347
|
+
RawAcl dacl = new RawAcl(GenericAcl.AclRevision, 3);
|
|
348
|
+
dacl.InsertAce(0, new CommonAce(AceFlags.None, AceQualifier.AccessAllowed,
|
|
349
|
+
FILE_ALL_ACCESS, user, false, null));
|
|
350
|
+
dacl.InsertAce(1, new CommonAce(AceFlags.None, AceQualifier.AccessAllowed,
|
|
351
|
+
FILE_ALL_ACCESS, system, false, null));
|
|
352
|
+
dacl.InsertAce(2, new CommonAce(AceFlags.None, AceQualifier.AccessAllowed,
|
|
353
|
+
FILE_ALL_ACCESS, admins, false, null));
|
|
354
|
+
RawSecurityDescriptor sd = new RawSecurityDescriptor(
|
|
355
|
+
ControlFlags.DiscretionaryAclPresent
|
|
356
|
+
| ControlFlags.DiscretionaryAclProtected
|
|
357
|
+
| ControlFlags.SelfRelative,
|
|
358
|
+
user, null, null, dacl);
|
|
359
|
+
byte[] bin = new byte[sd.BinaryLength];
|
|
360
|
+
sd.GetBinaryForm(bin, 0);
|
|
361
|
+
return bin;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Open a path no-follow on a FRESH handle. INVALID_HANDLE on failure with
|
|
365
|
+
// the win32 error in `err`. Caller MUST CloseHandle on success.
|
|
366
|
+
private static IntPtr OpenNoFollow(string path, uint access, uint share, out int err)
|
|
367
|
+
{
|
|
368
|
+
uint flags = FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS;
|
|
369
|
+
IntPtr h = CreateFileW(path, access, share, IntPtr.Zero, OPEN_EXISTING, flags, IntPtr.Zero);
|
|
370
|
+
err = Marshal.GetLastWin32Error();
|
|
371
|
+
return h;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Read attributes + opaque from an open handle.
|
|
375
|
+
private static bool ReadInfo(IntPtr h, out uint attributes, out string opaque, out bool zeroId)
|
|
376
|
+
{
|
|
377
|
+
attributes = 0; opaque = null; zeroId = true;
|
|
378
|
+
BY_HANDLE_FILE_INFORMATION info;
|
|
379
|
+
if (!GetFileInformationByHandle(h, out info)) return false;
|
|
380
|
+
attributes = info.dwFileAttributes;
|
|
381
|
+
opaque = BuildOpaque(info, out zeroId);
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Read owner + DACL from an open handle into managed form.
|
|
386
|
+
// Returns false and sets refusal on any failure.
|
|
387
|
+
private static bool ReadSd(IntPtr h, out SecurityIdentifier owner,
|
|
388
|
+
out RawAcl dacl, out bool nullDacl, out string failDetail)
|
|
389
|
+
{
|
|
390
|
+
owner = null; dacl = null; nullDacl = false; failDetail = null;
|
|
391
|
+
IntPtr pOwner, pGroup, pDacl, pSacl, pSd;
|
|
392
|
+
uint rc = GetSecurityInfo(h, SE_FILE_OBJECT,
|
|
393
|
+
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
|
|
394
|
+
out pOwner, out pGroup, out pDacl, out pSacl, out pSd);
|
|
395
|
+
if (rc != 0) { failDetail = "GetSecurityInfo " + rc; return false; }
|
|
396
|
+
try
|
|
397
|
+
{
|
|
398
|
+
int len = (int)GetSecurityDescriptorLength(pSd);
|
|
399
|
+
byte[] raw = new byte[len];
|
|
400
|
+
Marshal.Copy(pSd, raw, 0, len);
|
|
401
|
+
RawSecurityDescriptor rsd = new RawSecurityDescriptor(raw, 0);
|
|
402
|
+
owner = rsd.Owner;
|
|
403
|
+
dacl = rsd.DiscretionaryAcl;
|
|
404
|
+
nullDacl = (dacl == null); // NULL DACL grants everyone -> refuse
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
finally { LocalFree(pSd); }
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Evaluate a DACL against a refuse mask (Predicate A rules 2-6).
|
|
411
|
+
// objectEndanger = PATH_ENDANGER_DIR or PATH_ENDANGER_FILE, used to label
|
|
412
|
+
// a hit as "path-endangering" vs "add-child" (container add-child bits).
|
|
413
|
+
private static string EvaluateDacl(RawAcl dacl, bool nullDacl, int refuseMask, int objectEndanger)
|
|
414
|
+
{
|
|
415
|
+
if (nullDacl) return "null DACL";
|
|
416
|
+
if (dacl == null) return "null DACL";
|
|
417
|
+
GENERIC_MAPPING map = FileMapping();
|
|
418
|
+
for (int i = 0; i < dacl.Count; i++)
|
|
419
|
+
{
|
|
420
|
+
QualifiedAce qa = dacl[i] as QualifiedAce;
|
|
421
|
+
if (qa == null) return "unrecognized ACE";
|
|
422
|
+
if (qa.AceQualifier != AceQualifier.AccessAllowed) continue; // deny does not grant
|
|
423
|
+
if (((int)qa.AceFlags & (int)AceFlags.InheritOnly) != 0) continue; // IO template (rule 4)
|
|
424
|
+
string sid = qa.SecurityIdentifier.Value;
|
|
425
|
+
if (!IsForeignTrustee(sid)) continue;
|
|
426
|
+
uint m = (uint)qa.AccessMask;
|
|
427
|
+
MapGenericMask(ref m, ref map); // expand generic bits BEFORE masking (rule 3)
|
|
428
|
+
int expanded = (int)m;
|
|
429
|
+
int hit = expanded & refuseMask;
|
|
430
|
+
if (hit != 0)
|
|
431
|
+
{
|
|
432
|
+
string kind = ((hit & objectEndanger) != 0) ? "path-endangering" : "add-child";
|
|
433
|
+
return "foreign trustee " + sid + " " + kind;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return null; // clean
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private static string ChildPath(string dir, string name)
|
|
440
|
+
{
|
|
441
|
+
return Path.Combine(dir, name);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// True if `path` contains no entries other than "." / "..". enumOk is false
|
|
445
|
+
// when enumeration itself could not be performed (treated as fail-closed by
|
|
446
|
+
// callers). Identity is pinned by a held handle across the call site.
|
|
447
|
+
private static bool DirIsEmpty(string path, out bool enumOk)
|
|
448
|
+
{
|
|
449
|
+
enumOk = false;
|
|
450
|
+
WIN32_FIND_DATA fd;
|
|
451
|
+
IntPtr hf = FindFirstFileW(Path.Combine(path, "*"), out fd);
|
|
452
|
+
if (hf == INVALID_HANDLE) return false;
|
|
453
|
+
try
|
|
454
|
+
{
|
|
455
|
+
enumOk = true;
|
|
456
|
+
do
|
|
457
|
+
{
|
|
458
|
+
string n = fd.cFileName;
|
|
459
|
+
if (n == "." || n == "..") continue;
|
|
460
|
+
return false; // a real child -> not empty
|
|
461
|
+
} while (FindNextFileW(hf, out fd));
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
finally { FindClose(hf); }
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private static Held Lookup(string handleId)
|
|
468
|
+
{
|
|
469
|
+
if (handleId == null) return null;
|
|
470
|
+
Held held;
|
|
471
|
+
lock (Gate) { if (!Handles.TryGetValue(handleId, out held)) return null; }
|
|
472
|
+
return held;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Register a fresh open directory handle in the retained table.
|
|
476
|
+
private static string Register(IntPtr h, string path, string opaque)
|
|
477
|
+
{
|
|
478
|
+
lock (Gate)
|
|
479
|
+
{
|
|
480
|
+
HandleSeq++;
|
|
481
|
+
string id = HandleSeq.ToString();
|
|
482
|
+
Held held = new Held();
|
|
483
|
+
held.Handle = h; held.Path = path; held.Opaque = opaque;
|
|
484
|
+
Handles[id] = held;
|
|
485
|
+
return id;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// --- create-with-SD helpers (Predicate B, atomic at creation) -----------
|
|
490
|
+
private static IntPtr CreateFileWithSd(string path, uint access, uint disp, uint flags, out int err)
|
|
491
|
+
{
|
|
492
|
+
byte[] sd = BuildProtectedSd();
|
|
493
|
+
GCHandle gh = GCHandle.Alloc(sd, GCHandleType.Pinned);
|
|
494
|
+
IntPtr pSa = IntPtr.Zero;
|
|
495
|
+
try
|
|
496
|
+
{
|
|
497
|
+
SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
|
|
498
|
+
sa.nLength = Marshal.SizeOf(typeof(SECURITY_ATTRIBUTES));
|
|
499
|
+
sa.lpSecurityDescriptor = gh.AddrOfPinnedObject();
|
|
500
|
+
sa.bInheritHandle = 0;
|
|
501
|
+
pSa = Marshal.AllocHGlobal(sa.nLength);
|
|
502
|
+
Marshal.StructureToPtr(sa, pSa, false);
|
|
503
|
+
IntPtr h = CreateFileW(path, access, 0, pSa, disp, flags, IntPtr.Zero);
|
|
504
|
+
err = Marshal.GetLastWin32Error();
|
|
505
|
+
return h;
|
|
506
|
+
}
|
|
507
|
+
finally
|
|
508
|
+
{
|
|
509
|
+
if (pSa != IntPtr.Zero) Marshal.FreeHGlobal(pSa);
|
|
510
|
+
gh.Free();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private static bool CreateDirWithSd(string path, out int err)
|
|
515
|
+
{
|
|
516
|
+
byte[] sd = BuildProtectedSd();
|
|
517
|
+
GCHandle gh = GCHandle.Alloc(sd, GCHandleType.Pinned);
|
|
518
|
+
IntPtr pSa = IntPtr.Zero;
|
|
519
|
+
try
|
|
520
|
+
{
|
|
521
|
+
SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
|
|
522
|
+
sa.nLength = Marshal.SizeOf(typeof(SECURITY_ATTRIBUTES));
|
|
523
|
+
sa.lpSecurityDescriptor = gh.AddrOfPinnedObject();
|
|
524
|
+
sa.bInheritHandle = 0;
|
|
525
|
+
pSa = Marshal.AllocHGlobal(sa.nLength);
|
|
526
|
+
Marshal.StructureToPtr(sa, pSa, false);
|
|
527
|
+
bool ok = CreateDirectoryW(path, pSa);
|
|
528
|
+
err = Marshal.GetLastWin32Error();
|
|
529
|
+
return ok;
|
|
530
|
+
}
|
|
531
|
+
finally
|
|
532
|
+
{
|
|
533
|
+
if (pSa != IntPtr.Zero) Marshal.FreeHGlobal(pSa);
|
|
534
|
+
gh.Free();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ====================================================================
|
|
539
|
+
// OPS (one method per HelperOp; every path re-opens FRESH no-follow)
|
|
540
|
+
// ====================================================================
|
|
541
|
+
|
|
542
|
+
// openDir: retained parent-chain handle. Maps ONLY ERROR_FILE_NOT_FOUND /
|
|
543
|
+
// ERROR_PATH_NOT_FOUND to a notFound status; a junction OPENS (no-follow)
|
|
544
|
+
// and is refused by the reparse-attribute check with NO status (JDA6-001).
|
|
545
|
+
public static OpResult OpenDir(string path)
|
|
546
|
+
{
|
|
547
|
+
try
|
|
548
|
+
{
|
|
549
|
+
if (String.IsNullOrEmpty(path)) return OpResult.Fail(R_CHAIN, "openDir: empty path");
|
|
550
|
+
int err;
|
|
551
|
+
// Retained lock handle: share read+write but NOT delete.
|
|
552
|
+
IntPtr h = OpenNoFollow(path, READ_CONTROL | FILE_READ_ATTRIBUTES,
|
|
553
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE, out err);
|
|
554
|
+
if (h == INVALID_HANDLE)
|
|
555
|
+
{
|
|
556
|
+
if (err == ERROR_FILE_NOT_FOUND || err == ERROR_PATH_NOT_FOUND)
|
|
557
|
+
return OpResult.FailStatus(R_CHAIN, "openDir not found " + path, err);
|
|
558
|
+
return OpResult.Fail(R_CHAIN, "openDir failed " + err + " " + path);
|
|
559
|
+
}
|
|
560
|
+
bool keep = false;
|
|
561
|
+
try
|
|
562
|
+
{
|
|
563
|
+
uint attr; string opaque; bool zeroId;
|
|
564
|
+
if (!ReadInfo(h, out attr, out opaque, out zeroId))
|
|
565
|
+
return OpResult.Fail(R_CHAIN, "openDir info failed " + path);
|
|
566
|
+
if ((attr & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
|
|
567
|
+
return OpResult.Fail(R_CHAIN, "openDir reparse point " + path);
|
|
568
|
+
if ((attr & FILE_ATTRIBUTE_DIRECTORY) == 0)
|
|
569
|
+
return OpResult.Fail(R_CHAIN, "openDir not a directory " + path); // notFound=false
|
|
570
|
+
if (zeroId)
|
|
571
|
+
return OpResult.Fail(R_CHAIN, "openDir unresolvable identity " + path);
|
|
572
|
+
string id = Register(h, path, opaque);
|
|
573
|
+
keep = true;
|
|
574
|
+
OpResult r = OpResult.Good();
|
|
575
|
+
r.HandleId = id; r.Opaque = opaque; r.Attributes = (int)attr;
|
|
576
|
+
return r;
|
|
577
|
+
}
|
|
578
|
+
finally { if (!keep) CloseHandle(h); }
|
|
579
|
+
}
|
|
580
|
+
catch (Exception ex) { return OpResult.Fail(R_CHAIN, "openDir exception " + ex.Message); }
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// revalidate: fresh no-follow re-open; refuse reparse; compare fresh opaque
|
|
584
|
+
// against the held token (C2 / REPARSE-4). Never answers from a stale handle.
|
|
585
|
+
public static OpResult Revalidate(string path, string heldOpaque)
|
|
586
|
+
{
|
|
587
|
+
try
|
|
588
|
+
{
|
|
589
|
+
if (String.IsNullOrEmpty(heldOpaque))
|
|
590
|
+
return OpResult.Fail(R_CHAIN, "revalidate unresolvable identity " + path);
|
|
591
|
+
int err;
|
|
592
|
+
IntPtr h = OpenNoFollow(path, READ_CONTROL | FILE_READ_ATTRIBUTES,
|
|
593
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, out err);
|
|
594
|
+
if (h == INVALID_HANDLE)
|
|
595
|
+
return OpResult.Fail(R_CHAIN, "revalidate open failed " + err + " " + path);
|
|
596
|
+
try
|
|
597
|
+
{
|
|
598
|
+
uint attr; string opaque; bool zeroId;
|
|
599
|
+
if (!ReadInfo(h, out attr, out opaque, out zeroId))
|
|
600
|
+
return OpResult.Fail(R_CHAIN, "revalidate info failed " + path);
|
|
601
|
+
if ((attr & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
|
|
602
|
+
return OpResult.Fail(R_CHAIN, "revalidate reparse point " + path);
|
|
603
|
+
if (zeroId)
|
|
604
|
+
return OpResult.Fail(R_CHAIN, "revalidate unresolvable identity " + path);
|
|
605
|
+
if (!String.Equals(opaque, heldOpaque, StringComparison.OrdinalIgnoreCase))
|
|
606
|
+
return OpResult.Fail(R_CHAIN, "revalidate identity changed " + path);
|
|
607
|
+
return OpResult.Good();
|
|
608
|
+
}
|
|
609
|
+
finally { CloseHandle(h); }
|
|
610
|
+
}
|
|
611
|
+
catch (Exception ex) { return OpResult.Fail(R_CHAIN, "revalidate exception " + ex.Message); }
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// proveOwner: Predicate A rule 1 on a fresh no-follow handle.
|
|
615
|
+
public static OpResult ProveOwner(string path)
|
|
616
|
+
{
|
|
617
|
+
try
|
|
618
|
+
{
|
|
619
|
+
int err;
|
|
620
|
+
IntPtr h = OpenNoFollow(path, READ_CONTROL | FILE_READ_ATTRIBUTES,
|
|
621
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, out err);
|
|
622
|
+
if (h == INVALID_HANDLE)
|
|
623
|
+
return OpResult.Fail(R_DACL, "proveOwner open failed " + err + " " + path);
|
|
624
|
+
try
|
|
625
|
+
{
|
|
626
|
+
uint attr; string opaque; bool zeroId;
|
|
627
|
+
if (!ReadInfo(h, out attr, out opaque, out zeroId))
|
|
628
|
+
return OpResult.Fail(R_DACL, "proveOwner info failed " + path);
|
|
629
|
+
if ((attr & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
|
|
630
|
+
return OpResult.Fail(R_CHAIN, "proveOwner reparse point " + path);
|
|
631
|
+
SecurityIdentifier owner; RawAcl dacl; bool nullDacl; string sdErr;
|
|
632
|
+
if (!ReadSd(h, out owner, out dacl, out nullDacl, out sdErr))
|
|
633
|
+
return OpResult.Fail(R_DACL, "proveOwner " + sdErr + " " + path);
|
|
634
|
+
string osid = (owner == null) ? null : owner.Value;
|
|
635
|
+
if (!OwnerTrusted(osid))
|
|
636
|
+
return OpResult.Fail(R_DACL, "foreign owner " + osid);
|
|
637
|
+
return OpResult.Good();
|
|
638
|
+
}
|
|
639
|
+
finally { CloseHandle(h); }
|
|
640
|
+
}
|
|
641
|
+
catch (Exception ex) { return OpResult.Fail(R_DACL, "proveOwner exception " + ex.Message); }
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// proveDacl: Predicate A rules 2-6, object-type-aware mask, fresh handle.
|
|
645
|
+
public static OpResult ProveDacl(string path)
|
|
646
|
+
{
|
|
647
|
+
return ProveDaclInternal(path, false);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// proveContainer: proveManagedContainer -> owner trusted AND CREATE_PARENT_DIR
|
|
651
|
+
// add-child refusal (JDB-201/F1 + JDA-401). Containers are directories.
|
|
652
|
+
public static OpResult ProveContainer(string path)
|
|
653
|
+
{
|
|
654
|
+
return ProveDaclInternal(path, true);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private static OpResult ProveDaclInternal(string path, bool container)
|
|
658
|
+
{
|
|
659
|
+
// Disambiguate the audit trail: a container refusal must not be mislabeled
|
|
660
|
+
// "proveDacl" (mirror this tag in every detail string below).
|
|
661
|
+
string tag = container ? "proveContainer" : "proveDacl";
|
|
662
|
+
try
|
|
663
|
+
{
|
|
664
|
+
int err;
|
|
665
|
+
IntPtr h = OpenNoFollow(path, READ_CONTROL | FILE_READ_ATTRIBUTES,
|
|
666
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, out err);
|
|
667
|
+
if (h == INVALID_HANDLE)
|
|
668
|
+
return OpResult.Fail(R_DACL, tag + " open failed " + err + " " + path);
|
|
669
|
+
try
|
|
670
|
+
{
|
|
671
|
+
uint attr; string opaque; bool zeroId;
|
|
672
|
+
if (!ReadInfo(h, out attr, out opaque, out zeroId))
|
|
673
|
+
return OpResult.Fail(R_DACL, tag + " info failed " + path);
|
|
674
|
+
if ((attr & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
|
|
675
|
+
return OpResult.Fail(R_CHAIN, tag + " reparse point " + path);
|
|
676
|
+
bool isDir = (attr & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
|
677
|
+
SecurityIdentifier owner; RawAcl dacl; bool nullDacl; string sdErr;
|
|
678
|
+
if (!ReadSd(h, out owner, out dacl, out nullDacl, out sdErr))
|
|
679
|
+
return OpResult.Fail(R_DACL, tag + " " + sdErr + " " + path);
|
|
680
|
+
if (container)
|
|
681
|
+
{
|
|
682
|
+
string osid = (owner == null) ? null : owner.Value;
|
|
683
|
+
if (!OwnerTrusted(osid))
|
|
684
|
+
return OpResult.Fail(R_DACL, "foreign owner " + osid);
|
|
685
|
+
}
|
|
686
|
+
int objectEndanger = isDir ? PATH_ENDANGER_DIR : PATH_ENDANGER_FILE;
|
|
687
|
+
int refuseMask = container ? CREATE_PARENT_DIR : objectEndanger;
|
|
688
|
+
string detail = EvaluateDacl(dacl, nullDacl, refuseMask, objectEndanger);
|
|
689
|
+
if (detail != null) return OpResult.Fail(R_DACL, detail);
|
|
690
|
+
return OpResult.Good();
|
|
691
|
+
}
|
|
692
|
+
finally { CloseHandle(h); }
|
|
693
|
+
}
|
|
694
|
+
catch (Exception ex) { return OpResult.Fail(R_DACL, tag + " exception " + ex.Message); }
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// createDir: CREATE_NEW (exclusive) directory relative to the parent's
|
|
698
|
+
// locked path, born with the Predicate B protected owner-only DACL, then
|
|
699
|
+
// re-opened no-follow and retained (defense in depth re-proves TS-side).
|
|
700
|
+
public static OpResult CreateDir(string parentHandleId, string name)
|
|
701
|
+
{
|
|
702
|
+
try
|
|
703
|
+
{
|
|
704
|
+
Held parent = Lookup(parentHandleId);
|
|
705
|
+
if (parent == null) return OpResult.Fail(R_CHAIN, "createDir unknown parent handle");
|
|
706
|
+
if (String.IsNullOrEmpty(name)) return OpResult.Fail(R_CHAIN, "createDir empty name");
|
|
707
|
+
string full = ChildPath(parent.Path, name);
|
|
708
|
+
int err;
|
|
709
|
+
if (!CreateDirWithSd(full, out err))
|
|
710
|
+
{
|
|
711
|
+
if (err == ERROR_ALREADY_EXISTS || err == ERROR_FILE_EXISTS)
|
|
712
|
+
return OpResult.Fail(R_CHAIN, "createDir exists " + full); // O_EXCL analog
|
|
713
|
+
return OpResult.Fail(R_CHAIN, "createDir failed " + err + " " + full);
|
|
714
|
+
}
|
|
715
|
+
IntPtr h = OpenNoFollow(full, READ_CONTROL | FILE_READ_ATTRIBUTES,
|
|
716
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE, out err);
|
|
717
|
+
if (h == INVALID_HANDLE)
|
|
718
|
+
return OpResult.Fail(R_CHAIN, "createDir reopen failed " + err + " " + full);
|
|
719
|
+
bool keep = false;
|
|
720
|
+
try
|
|
721
|
+
{
|
|
722
|
+
uint attr; string opaque; bool zeroId;
|
|
723
|
+
if (!ReadInfo(h, out attr, out opaque, out zeroId))
|
|
724
|
+
return OpResult.Fail(R_CHAIN, "createDir info failed " + full);
|
|
725
|
+
if ((attr & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
|
|
726
|
+
return OpResult.Fail(R_CHAIN, "createDir reparse point " + full);
|
|
727
|
+
if ((attr & FILE_ATTRIBUTE_DIRECTORY) == 0)
|
|
728
|
+
return OpResult.Fail(R_CHAIN, "createDir not a directory " + full);
|
|
729
|
+
if (zeroId)
|
|
730
|
+
return OpResult.Fail(R_CHAIN, "createDir unresolvable identity " + full);
|
|
731
|
+
string id = Register(h, full, opaque);
|
|
732
|
+
keep = true;
|
|
733
|
+
OpResult r = OpResult.Good();
|
|
734
|
+
r.HandleId = id; r.Opaque = opaque; r.Attributes = (int)attr;
|
|
735
|
+
return r;
|
|
736
|
+
}
|
|
737
|
+
finally { if (!keep) CloseHandle(h); }
|
|
738
|
+
}
|
|
739
|
+
catch (Exception ex) { return OpResult.Fail(R_CHAIN, "createDir exception " + ex.Message); }
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// capture: S_ISREG-equivalent regular-file assert on a fresh no-follow
|
|
743
|
+
// handle; returns base64 bytes + opaque. Never dereferences a reparse point.
|
|
744
|
+
public static OpResult Capture(string path)
|
|
745
|
+
{
|
|
746
|
+
try
|
|
747
|
+
{
|
|
748
|
+
int err;
|
|
749
|
+
IntPtr h = OpenNoFollow(path, GENERIC_READ | FILE_READ_ATTRIBUTES,
|
|
750
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, out err);
|
|
751
|
+
if (h == INVALID_HANDLE)
|
|
752
|
+
return OpResult.Fail(R_CHAIN, "capture open failed " + err + " " + path);
|
|
753
|
+
try
|
|
754
|
+
{
|
|
755
|
+
uint attr; string opaque; bool zeroId;
|
|
756
|
+
if (!ReadInfo(h, out attr, out opaque, out zeroId))
|
|
757
|
+
return OpResult.Fail(R_CHAIN, "capture info failed " + path);
|
|
758
|
+
if ((attr & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
|
|
759
|
+
return OpResult.Fail(R_CHAIN, "capture reparse point " + path);
|
|
760
|
+
if ((attr & FILE_ATTRIBUTE_DIRECTORY) != 0)
|
|
761
|
+
return OpResult.Fail(R_CHAIN, "capture not a regular file " + path);
|
|
762
|
+
if (zeroId)
|
|
763
|
+
return OpResult.Fail(R_CHAIN, "capture unresolvable identity " + path);
|
|
764
|
+
byte[] bytes;
|
|
765
|
+
using (SafeFileHandle sfh = new SafeFileHandle(h, false))
|
|
766
|
+
using (FileStream fs = new FileStream(sfh, FileAccess.Read))
|
|
767
|
+
using (MemoryStream ms = new MemoryStream())
|
|
768
|
+
{
|
|
769
|
+
fs.CopyTo(ms);
|
|
770
|
+
bytes = ms.ToArray();
|
|
771
|
+
}
|
|
772
|
+
OpResult r = OpResult.Good();
|
|
773
|
+
r.BytesB64 = Convert.ToBase64String(bytes);
|
|
774
|
+
r.Opaque = opaque;
|
|
775
|
+
return r;
|
|
776
|
+
}
|
|
777
|
+
finally { CloseHandle(h); }
|
|
778
|
+
}
|
|
779
|
+
catch (Exception ex) { return OpResult.Fail(R_CHAIN, "capture exception " + ex.Message); }
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// writeExcl: CREATE_NEW file relative to the locked dir path, born with the
|
|
783
|
+
// Predicate B protected owner-only DACL; O_EXCL refusal on a pre-existing
|
|
784
|
+
// target; flush before close.
|
|
785
|
+
public static OpResult WriteExcl(string dirHandleId, string name, string bytesB64)
|
|
786
|
+
{
|
|
787
|
+
try
|
|
788
|
+
{
|
|
789
|
+
Held dir = Lookup(dirHandleId);
|
|
790
|
+
if (dir == null) return OpResult.Fail(R_CHAIN, "writeExcl unknown dir handle");
|
|
791
|
+
if (String.IsNullOrEmpty(name)) return OpResult.Fail(R_CHAIN, "writeExcl empty name");
|
|
792
|
+
byte[] bytes;
|
|
793
|
+
try { bytes = Convert.FromBase64String(bytesB64 == null ? "" : bytesB64); }
|
|
794
|
+
catch { return OpResult.Fail(R_CHAIN, "writeExcl bad base64"); }
|
|
795
|
+
string full = ChildPath(dir.Path, name);
|
|
796
|
+
int err;
|
|
797
|
+
IntPtr h = CreateFileWithSd(full, GENERIC_WRITE, CREATE_NEW,
|
|
798
|
+
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OPEN_REPARSE_POINT, out err);
|
|
799
|
+
if (h == INVALID_HANDLE)
|
|
800
|
+
{
|
|
801
|
+
if (err == ERROR_FILE_EXISTS || err == ERROR_ALREADY_EXISTS)
|
|
802
|
+
return OpResult.Fail(R_CHAIN, "writeExcl exists " + full); // O_EXCL analog
|
|
803
|
+
return OpResult.Fail(R_CHAIN, "writeExcl failed " + err + " " + full);
|
|
804
|
+
}
|
|
805
|
+
try
|
|
806
|
+
{
|
|
807
|
+
using (SafeFileHandle sfh = new SafeFileHandle(h, false))
|
|
808
|
+
using (FileStream fs = new FileStream(sfh, FileAccess.Write))
|
|
809
|
+
{
|
|
810
|
+
fs.Write(bytes, 0, bytes.Length);
|
|
811
|
+
fs.Flush();
|
|
812
|
+
}
|
|
813
|
+
FlushFileBuffers(h);
|
|
814
|
+
return OpResult.Good();
|
|
815
|
+
}
|
|
816
|
+
finally { CloseHandle(h); }
|
|
817
|
+
}
|
|
818
|
+
catch (Exception ex) { return OpResult.Fail(R_CHAIN, "writeExcl exception " + ex.Message); }
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// applyMode: NOT a numeric mode. Re-open no-follow, re-assert the Predicate B
|
|
822
|
+
// protected owner-only DACL (idempotent, repairs drift), then re-prove
|
|
823
|
+
// Predicate A on a fresh handle (Decision 1a).
|
|
824
|
+
public static OpResult ApplyMode(string path)
|
|
825
|
+
{
|
|
826
|
+
try
|
|
827
|
+
{
|
|
828
|
+
int err;
|
|
829
|
+
IntPtr h = OpenNoFollow(path,
|
|
830
|
+
WRITE_DAC_A | WRITE_OWNER_A | READ_CONTROL | FILE_READ_ATTRIBUTES,
|
|
831
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE, out err);
|
|
832
|
+
if (h == INVALID_HANDLE)
|
|
833
|
+
return OpResult.Fail(R_CHAIN, "applyMode open failed " + err + " " + path);
|
|
834
|
+
try
|
|
835
|
+
{
|
|
836
|
+
uint attr; string opaque; bool zeroId;
|
|
837
|
+
if (!ReadInfo(h, out attr, out opaque, out zeroId))
|
|
838
|
+
return OpResult.Fail(R_CHAIN, "applyMode info failed " + path);
|
|
839
|
+
if ((attr & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
|
|
840
|
+
return OpResult.Fail(R_CHAIN, "applyMode reparse point " + path);
|
|
841
|
+
byte[] sd = BuildProtectedSd();
|
|
842
|
+
if (!SetKernelObjectSecurity(h,
|
|
843
|
+
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION
|
|
844
|
+
| PROTECTED_DACL_SECURITY_INFORMATION, sd))
|
|
845
|
+
return OpResult.Fail(R_DACL, "applyMode set security " + Marshal.GetLastWin32Error());
|
|
846
|
+
}
|
|
847
|
+
finally { CloseHandle(h); }
|
|
848
|
+
// Re-prove Predicate A on a FRESH handle (file object).
|
|
849
|
+
return ProveDacl(path);
|
|
850
|
+
}
|
|
851
|
+
catch (Exception ex) { return OpResult.Fail(R_DACL, "applyMode exception " + ex.Message); }
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// rename: MoveFileEx REPLACE_EXISTING | WRITE_THROUGH within the locked dir,
|
|
855
|
+
// then FlushFileBuffers on the retained dir handle (durable-commit).
|
|
856
|
+
public static OpResult Rename(string dirHandleId, string from, string to)
|
|
857
|
+
{
|
|
858
|
+
try
|
|
859
|
+
{
|
|
860
|
+
Held dir = Lookup(dirHandleId);
|
|
861
|
+
if (dir == null) return OpResult.Fail(R_CHAIN, "rename unknown dir handle");
|
|
862
|
+
if (String.IsNullOrEmpty(from) || String.IsNullOrEmpty(to))
|
|
863
|
+
return OpResult.Fail(R_CHAIN, "rename empty name");
|
|
864
|
+
string fromFull = ChildPath(dir.Path, from);
|
|
865
|
+
string toFull = ChildPath(dir.Path, to);
|
|
866
|
+
// No opaque re-check here (unlike unlink/rmdir): both endpoints are our
|
|
867
|
+
// own just-created nodes under the held parent-chain lock, so no on-path
|
|
868
|
+
// node can be swapped while the chain handle is held.
|
|
869
|
+
if (!MoveFileExW(fromFull, toFull,
|
|
870
|
+
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
|
|
871
|
+
return OpResult.Fail(R_CHAIN, "rename failed " + Marshal.GetLastWin32Error()
|
|
872
|
+
+ " " + fromFull);
|
|
873
|
+
FlushFileBuffers(dir.Handle);
|
|
874
|
+
return OpResult.Good();
|
|
875
|
+
}
|
|
876
|
+
catch (Exception ex) { return OpResult.Fail(R_CHAIN, "rename exception " + ex.Message); }
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// unlink: open the child no-follow relative to the locked dir path, verify
|
|
880
|
+
// full-precision identity against the held opaque (C4), then delete via
|
|
881
|
+
// FileDispositionInfo. Zero/absent/changed identity refuses.
|
|
882
|
+
public static OpResult Unlink(string dirHandleId, string name, string heldOpaque)
|
|
883
|
+
{
|
|
884
|
+
try
|
|
885
|
+
{
|
|
886
|
+
Held dir = Lookup(dirHandleId);
|
|
887
|
+
if (dir == null) return OpResult.Fail(R_CHAIN, "unlink unknown dir handle");
|
|
888
|
+
if (String.IsNullOrEmpty(name)) return OpResult.Fail(R_CHAIN, "unlink empty name");
|
|
889
|
+
if (String.IsNullOrEmpty(heldOpaque))
|
|
890
|
+
return OpResult.Fail(R_CHAIN, "unlink unresolvable identity " + name);
|
|
891
|
+
string full = ChildPath(dir.Path, name);
|
|
892
|
+
int err;
|
|
893
|
+
IntPtr h = OpenNoFollow(full, DELETE_A | FILE_READ_ATTRIBUTES,
|
|
894
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, out err);
|
|
895
|
+
if (h == INVALID_HANDLE)
|
|
896
|
+
return OpResult.Fail(R_CHAIN, "unlink open failed " + err + " " + full);
|
|
897
|
+
try
|
|
898
|
+
{
|
|
899
|
+
uint attr; string opaque; bool zeroId;
|
|
900
|
+
if (!ReadInfo(h, out attr, out opaque, out zeroId))
|
|
901
|
+
return OpResult.Fail(R_CHAIN, "unlink info failed " + full);
|
|
902
|
+
if ((attr & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
|
|
903
|
+
return OpResult.Fail(R_CHAIN, "unlink reparse point " + full);
|
|
904
|
+
if (zeroId)
|
|
905
|
+
return OpResult.Fail(R_CHAIN, "unlink unresolvable identity " + full);
|
|
906
|
+
if (!String.Equals(opaque, heldOpaque, StringComparison.OrdinalIgnoreCase))
|
|
907
|
+
return OpResult.Fail(R_CHAIN, "unlink identity changed " + full);
|
|
908
|
+
FILE_DISPOSITION_INFO info = new FILE_DISPOSITION_INFO();
|
|
909
|
+
info.DeleteFile = 1;
|
|
910
|
+
if (!SetFileInformationByHandle(h, FileDispositionInfo, ref info,
|
|
911
|
+
Marshal.SizeOf(typeof(FILE_DISPOSITION_INFO))))
|
|
912
|
+
return OpResult.Fail(R_CHAIN, "unlink delete failed " + Marshal.GetLastWin32Error());
|
|
913
|
+
return OpResult.Good();
|
|
914
|
+
}
|
|
915
|
+
finally { CloseHandle(h); }
|
|
916
|
+
}
|
|
917
|
+
catch (Exception ex) { return OpResult.Fail(R_CHAIN, "unlink exception " + ex.Message); }
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// rmdir: delete a tx-created empty directory (Phase-5 CI-validated: rmdir of a
|
|
921
|
+
// tx-created empty dir succeeds; rmdir of a non-empty or identity-drifted dir
|
|
922
|
+
// refuses).
|
|
923
|
+
//
|
|
924
|
+
// WHY the release/reopen dance (do NOT collapse it to RemoveDirectoryW on the
|
|
925
|
+
// retained handle): the retained lock handle (dir.Handle, opened in OpenDir /
|
|
926
|
+
// CreateDir) was opened with FILE_SHARE_READ|FILE_SHARE_WRITE and NO
|
|
927
|
+
// FILE_SHARE_DELETE. On Win32 a DELETE-access open -- which RemoveDirectory and
|
|
928
|
+
// delete-on-close both perform internally -- requires EVERY existing handle to
|
|
929
|
+
// the target to permit FILE_SHARE_DELETE. Our own retained no-share-delete
|
|
930
|
+
// handle therefore blocks deletion of the very directory it protects, failing
|
|
931
|
+
// with ERROR_SHARING_VIOLATION (32). So we MUST release the retained handle
|
|
932
|
+
// before deleting. To keep that release->reopen window safe we bracket it with
|
|
933
|
+
// a double opaque re-check: verify+capture identity on the retained handle,
|
|
934
|
+
// release it, re-open no-follow for DELETE with FILE_SHARE_DELETE, then
|
|
935
|
+
// re-verify the SAME opaque on the fresh handle (catches a swap in the tiny
|
|
936
|
+
// gap). Deletion is delete-on-close on that fresh handle, so we never issue a
|
|
937
|
+
// by-path RemoveDirectoryW (which would reintroduce a by-path TOCTOU).
|
|
938
|
+
// Fail-closed: any drift / non-empty refuses and the dir is left
|
|
939
|
+
// released-but-present, which is safe for a tx-created empty dir.
|
|
940
|
+
public static OpResult Rmdir(string dirHandleId, string heldOpaque)
|
|
941
|
+
{
|
|
942
|
+
try
|
|
943
|
+
{
|
|
944
|
+
Held dir = Lookup(dirHandleId);
|
|
945
|
+
if (dir == null) return OpResult.Fail(R_CHAIN, "rmdir unknown handle");
|
|
946
|
+
if (String.IsNullOrEmpty(heldOpaque))
|
|
947
|
+
return OpResult.Fail(R_CHAIN, "rmdir unresolvable identity " + dir.Path);
|
|
948
|
+
|
|
949
|
+
// 1) Verify identity + emptiness on the RETAINED handle; capture opaque.
|
|
950
|
+
// A kernel handle is bound to its file object, so its identity cannot
|
|
951
|
+
// drift; emptiness is enumerated by-path while the retained handle
|
|
952
|
+
// (no delete share) pins the directory against rename/delete.
|
|
953
|
+
uint attr0; string opaque0; bool zeroId0;
|
|
954
|
+
if (!ReadInfo(dir.Handle, out attr0, out opaque0, out zeroId0))
|
|
955
|
+
return OpResult.Fail(R_CHAIN, "rmdir info failed " + dir.Path);
|
|
956
|
+
if ((attr0 & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
|
|
957
|
+
return OpResult.Fail(R_CHAIN, "rmdir reparse point " + dir.Path);
|
|
958
|
+
if (zeroId0)
|
|
959
|
+
return OpResult.Fail(R_CHAIN, "rmdir unresolvable identity " + dir.Path);
|
|
960
|
+
if (!String.Equals(opaque0, heldOpaque, StringComparison.OrdinalIgnoreCase))
|
|
961
|
+
return OpResult.Fail(R_CHAIN, "rmdir identity changed " + dir.Path);
|
|
962
|
+
bool enumOk0;
|
|
963
|
+
if (!DirIsEmpty(dir.Path, out enumOk0))
|
|
964
|
+
{
|
|
965
|
+
if (!enumOk0) return OpResult.Fail(R_CHAIN, "rmdir enumerate failed " + dir.Path);
|
|
966
|
+
return OpResult.Fail(R_CHAIN, "rmdir not empty " + dir.Path);
|
|
967
|
+
}
|
|
968
|
+
string captured = opaque0;
|
|
969
|
+
|
|
970
|
+
// 2) Release the retained no-share-delete handle so it stops blocking
|
|
971
|
+
// the DELETE-access open below.
|
|
972
|
+
CloseAndForget(dirHandleId);
|
|
973
|
+
|
|
974
|
+
// 3) Re-open the path no-follow for DELETE, sharing delete this time.
|
|
975
|
+
int err;
|
|
976
|
+
IntPtr h = OpenNoFollow(dir.Path, DELETE_A | FILE_READ_ATTRIBUTES,
|
|
977
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, out err);
|
|
978
|
+
if (h == INVALID_HANDLE)
|
|
979
|
+
return OpResult.Fail(R_CHAIN, "rmdir delete-open failed " + err + " " + dir.Path);
|
|
980
|
+
try
|
|
981
|
+
{
|
|
982
|
+
// 4a) Re-verify identity on the fresh handle: refuse reparse / zero
|
|
983
|
+
// id, and require the SAME opaque captured in step 1 (this closes
|
|
984
|
+
// the tiny close->reopen swap window).
|
|
985
|
+
uint attr1; string opaque1; bool zeroId1;
|
|
986
|
+
if (!ReadInfo(h, out attr1, out opaque1, out zeroId1))
|
|
987
|
+
return OpResult.Fail(R_CHAIN, "rmdir info failed " + dir.Path);
|
|
988
|
+
if ((attr1 & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
|
|
989
|
+
return OpResult.Fail(R_CHAIN, "rmdir reparse point " + dir.Path);
|
|
990
|
+
if (zeroId1)
|
|
991
|
+
return OpResult.Fail(R_CHAIN, "rmdir unresolvable identity " + dir.Path);
|
|
992
|
+
if (!String.Equals(opaque1, captured, StringComparison.OrdinalIgnoreCase))
|
|
993
|
+
return OpResult.Fail(R_CHAIN, "rmdir identity changed " + dir.Path);
|
|
994
|
+
// 4b) Re-verify emptiness now that we hold the fresh handle.
|
|
995
|
+
bool enumOk1;
|
|
996
|
+
if (!DirIsEmpty(dir.Path, out enumOk1))
|
|
997
|
+
{
|
|
998
|
+
if (!enumOk1) return OpResult.Fail(R_CHAIN, "rmdir enumerate failed " + dir.Path);
|
|
999
|
+
return OpResult.Fail(R_CHAIN, "rmdir not empty " + dir.Path);
|
|
1000
|
+
}
|
|
1001
|
+
// 5) Delete-on-close: FileDispositionInfo removes the directory when
|
|
1002
|
+
// this fresh handle closes. It also returns ERROR_DIR_NOT_EMPTY if
|
|
1003
|
+
// a child slipped in after 4b -> fail-closed, no by-path TOCTOU.
|
|
1004
|
+
FILE_DISPOSITION_INFO info = new FILE_DISPOSITION_INFO();
|
|
1005
|
+
info.DeleteFile = 1;
|
|
1006
|
+
if (!SetFileInformationByHandle(h, FileDispositionInfo, ref info,
|
|
1007
|
+
Marshal.SizeOf(typeof(FILE_DISPOSITION_INFO))))
|
|
1008
|
+
{
|
|
1009
|
+
int derr = Marshal.GetLastWin32Error();
|
|
1010
|
+
if (derr == ERROR_DIR_NOT_EMPTY)
|
|
1011
|
+
return OpResult.Fail(R_CHAIN, "rmdir not empty " + dir.Path);
|
|
1012
|
+
return OpResult.Fail(R_CHAIN, "rmdir delete failed " + derr + " " + dir.Path);
|
|
1013
|
+
}
|
|
1014
|
+
return OpResult.Good();
|
|
1015
|
+
}
|
|
1016
|
+
finally { CloseHandle(h); }
|
|
1017
|
+
}
|
|
1018
|
+
catch (Exception ex) { return OpResult.Fail(R_CHAIN, "rmdir exception " + ex.Message); }
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// releaseHandle: always succeeds (the adapter balances the session handle
|
|
1022
|
+
// count on ANY ok openDir/createDir, even a null/unknown id).
|
|
1023
|
+
public static OpResult ReleaseHandle(string handleId)
|
|
1024
|
+
{
|
|
1025
|
+
try { CloseAndForget(handleId); }
|
|
1026
|
+
catch { /* fail-open on release is safe: the process exit hook kills all */ }
|
|
1027
|
+
return OpResult.Good();
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
private static void CloseAndForget(string handleId)
|
|
1031
|
+
{
|
|
1032
|
+
if (handleId == null) return;
|
|
1033
|
+
Held held = null;
|
|
1034
|
+
lock (Gate)
|
|
1035
|
+
{
|
|
1036
|
+
if (Handles.TryGetValue(handleId, out held)) Handles.Remove(handleId);
|
|
1037
|
+
}
|
|
1038
|
+
if (held != null && held.Handle != IntPtr.Zero && held.Handle != INVALID_HANDLE)
|
|
1039
|
+
CloseHandle(held.Handle);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Best-effort close of every retained handle at process shutdown.
|
|
1043
|
+
public static void CloseAll()
|
|
1044
|
+
{
|
|
1045
|
+
lock (Gate)
|
|
1046
|
+
{
|
|
1047
|
+
foreach (KeyValuePair<string, Held> kv in Handles)
|
|
1048
|
+
{
|
|
1049
|
+
try
|
|
1050
|
+
{
|
|
1051
|
+
if (kv.Value.Handle != IntPtr.Zero && kv.Value.Handle != INVALID_HANDLE)
|
|
1052
|
+
CloseHandle(kv.Value.Handle);
|
|
1053
|
+
}
|
|
1054
|
+
catch { }
|
|
1055
|
+
}
|
|
1056
|
+
Handles.Clear();
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
'@
|
|
1062
|
+
|
|
1063
|
+
Add-Type -TypeDefinition $csharp -Language CSharp | Out-Null
|
|
1064
|
+
|
|
1065
|
+
# --- framed raw binary stdio (W2: only frames on stdout, diagnostics to stderr) -
|
|
1066
|
+
|
|
1067
|
+
$script:stdin = [Console]::OpenStandardInput()
|
|
1068
|
+
$script:stdout = [Console]::OpenStandardOutput()
|
|
1069
|
+
|
|
1070
|
+
function Read-Exact([int]$count) {
|
|
1071
|
+
if ($count -eq 0) { return , (New-Object byte[] 0) }
|
|
1072
|
+
$buf = New-Object byte[] $count
|
|
1073
|
+
$off = 0
|
|
1074
|
+
while ($off -lt $count) {
|
|
1075
|
+
$r = $script:stdin.Read($buf, $off, $count - $off)
|
|
1076
|
+
if ($r -le 0) { return $null } # EOF -> parent closed stdin
|
|
1077
|
+
$off += $r
|
|
1078
|
+
}
|
|
1079
|
+
return , $buf
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function Write-Frame($obj) {
|
|
1083
|
+
$json = ConvertTo-Json $obj -Compress -Depth 6
|
|
1084
|
+
$bytes = [Text.Encoding]::UTF8.GetBytes($json)
|
|
1085
|
+
$len = $bytes.Length
|
|
1086
|
+
$hdr = New-Object byte[] 4
|
|
1087
|
+
$hdr[0] = [byte](($len -shr 24) -band 0xFF)
|
|
1088
|
+
$hdr[1] = [byte](($len -shr 16) -band 0xFF)
|
|
1089
|
+
$hdr[2] = [byte](($len -shr 8) -band 0xFF)
|
|
1090
|
+
$hdr[3] = [byte]($len -band 0xFF)
|
|
1091
|
+
$script:stdout.Write($hdr, 0, 4)
|
|
1092
|
+
$script:stdout.Write($bytes, 0, $len)
|
|
1093
|
+
$script:stdout.Flush()
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
# --- response shaping (must match HelperResponse in secure-fs-windows.ts) ------
|
|
1097
|
+
|
|
1098
|
+
function Send-Void($r) {
|
|
1099
|
+
if ($r.Ok) { Write-Frame @{ ok = $true } ; return }
|
|
1100
|
+
$resp = @{ ok = $false }
|
|
1101
|
+
if ($r.Refusal) { $resp['refusal'] = $r.Refusal }
|
|
1102
|
+
if ($r.Detail) { $resp['detail'] = $r.Detail }
|
|
1103
|
+
Write-Frame $resp
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function Send-Handle($r) {
|
|
1107
|
+
if ($r.Ok) {
|
|
1108
|
+
Write-Frame @{ ok = $true; value = @{ handleId = $r.HandleId; opaque = $r.Opaque; attributes = $r.Attributes } }
|
|
1109
|
+
return
|
|
1110
|
+
}
|
|
1111
|
+
$resp = @{ ok = $false }
|
|
1112
|
+
if ($r.Refusal) { $resp['refusal'] = $r.Refusal }
|
|
1113
|
+
if ($r.Detail) { $resp['detail'] = $r.Detail }
|
|
1114
|
+
if ($r.Status -gt 0) { $resp['status'] = $r.Status }
|
|
1115
|
+
Write-Frame $resp
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function Send-Capture($r) {
|
|
1119
|
+
if ($r.Ok) {
|
|
1120
|
+
Write-Frame @{ ok = $true; value = @{ bytes = $r.BytesB64; opaque = $r.Opaque } }
|
|
1121
|
+
return
|
|
1122
|
+
}
|
|
1123
|
+
$resp = @{ ok = $false }
|
|
1124
|
+
if ($r.Refusal) { $resp['refusal'] = $r.Refusal }
|
|
1125
|
+
if ($r.Detail) { $resp['detail'] = $r.Detail }
|
|
1126
|
+
Write-Frame $resp
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function Get-Arg($obj, [string]$name) {
|
|
1130
|
+
if ($null -eq $obj) { return $null }
|
|
1131
|
+
$p = $obj.PSObject.Properties[$name]
|
|
1132
|
+
if ($null -eq $p) { return $null }
|
|
1133
|
+
return $p.Value
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function AsStr($v) {
|
|
1137
|
+
if ($null -eq $v) { return $null }
|
|
1138
|
+
return [string]$v
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
# --- session loop -------------------------------------------------------------
|
|
1142
|
+
|
|
1143
|
+
Write-Frame @{ ready = $true; protocolVersion = $script:PROTOCOL_VERSION }
|
|
1144
|
+
|
|
1145
|
+
try {
|
|
1146
|
+
while ($true) {
|
|
1147
|
+
$hdr = Read-Exact 4
|
|
1148
|
+
if ($null -eq $hdr) { break } # EOF -> clean shutdown
|
|
1149
|
+
$len = ([int]$hdr[0] -shl 24) -bor ([int]$hdr[1] -shl 16) -bor ([int]$hdr[2] -shl 8) -bor [int]$hdr[3]
|
|
1150
|
+
if ($len -lt 0 -or $len -gt $script:FRAME_LIMIT) {
|
|
1151
|
+
[Console]::Error.WriteLine("oversized request frame: $len")
|
|
1152
|
+
break
|
|
1153
|
+
}
|
|
1154
|
+
$body = Read-Exact $len
|
|
1155
|
+
if ($null -eq $body) { break }
|
|
1156
|
+
|
|
1157
|
+
$req = $null
|
|
1158
|
+
try {
|
|
1159
|
+
$req = [Text.Encoding]::UTF8.GetString($body) | ConvertFrom-Json
|
|
1160
|
+
}
|
|
1161
|
+
catch {
|
|
1162
|
+
Write-Frame @{ ok = $false; refusal = 'unsafe-parent-chain'; detail = 'malformed request frame' }
|
|
1163
|
+
continue
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
$op = AsStr (Get-Arg $req 'op')
|
|
1167
|
+
$a = Get-Arg $req 'args'
|
|
1168
|
+
|
|
1169
|
+
try {
|
|
1170
|
+
switch ($op) {
|
|
1171
|
+
'openDir' {
|
|
1172
|
+
Send-Handle ([JaviForge.SecureObj]::OpenDir((AsStr (Get-Arg $a 'path'))))
|
|
1173
|
+
}
|
|
1174
|
+
'revalidate' {
|
|
1175
|
+
Send-Void ([JaviForge.SecureObj]::Revalidate((AsStr (Get-Arg $a 'path')), (AsStr (Get-Arg $a 'opaque'))))
|
|
1176
|
+
}
|
|
1177
|
+
'proveOwner' {
|
|
1178
|
+
Send-Void ([JaviForge.SecureObj]::ProveOwner((AsStr (Get-Arg $a 'path'))))
|
|
1179
|
+
}
|
|
1180
|
+
'proveDacl' {
|
|
1181
|
+
Send-Void ([JaviForge.SecureObj]::ProveDacl((AsStr (Get-Arg $a 'path'))))
|
|
1182
|
+
}
|
|
1183
|
+
'proveContainer' {
|
|
1184
|
+
Send-Void ([JaviForge.SecureObj]::ProveContainer((AsStr (Get-Arg $a 'path'))))
|
|
1185
|
+
}
|
|
1186
|
+
'createDir' {
|
|
1187
|
+
Send-Handle ([JaviForge.SecureObj]::CreateDir((AsStr (Get-Arg $a 'parentHandle')), (AsStr (Get-Arg $a 'name'))))
|
|
1188
|
+
}
|
|
1189
|
+
'capture' {
|
|
1190
|
+
Send-Capture ([JaviForge.SecureObj]::Capture((AsStr (Get-Arg $a 'path'))))
|
|
1191
|
+
}
|
|
1192
|
+
'writeExcl' {
|
|
1193
|
+
Send-Void ([JaviForge.SecureObj]::WriteExcl((AsStr (Get-Arg $a 'dirHandle')), (AsStr (Get-Arg $a 'name')), (AsStr (Get-Arg $a 'bytes'))))
|
|
1194
|
+
}
|
|
1195
|
+
'applyMode' {
|
|
1196
|
+
Send-Void ([JaviForge.SecureObj]::ApplyMode((AsStr (Get-Arg $a 'path'))))
|
|
1197
|
+
}
|
|
1198
|
+
'rename' {
|
|
1199
|
+
Send-Void ([JaviForge.SecureObj]::Rename((AsStr (Get-Arg $a 'dirHandle')), (AsStr (Get-Arg $a 'from')), (AsStr (Get-Arg $a 'to'))))
|
|
1200
|
+
}
|
|
1201
|
+
'unlink' {
|
|
1202
|
+
Send-Void ([JaviForge.SecureObj]::Unlink((AsStr (Get-Arg $a 'dirHandle')), (AsStr (Get-Arg $a 'name')), (AsStr (Get-Arg $a 'opaque'))))
|
|
1203
|
+
}
|
|
1204
|
+
'rmdir' {
|
|
1205
|
+
Send-Void ([JaviForge.SecureObj]::Rmdir((AsStr (Get-Arg $a 'handle')), (AsStr (Get-Arg $a 'opaque'))))
|
|
1206
|
+
}
|
|
1207
|
+
'releaseHandle' {
|
|
1208
|
+
Send-Void ([JaviForge.SecureObj]::ReleaseHandle((AsStr (Get-Arg $a 'handle'))))
|
|
1209
|
+
}
|
|
1210
|
+
default {
|
|
1211
|
+
Write-Frame @{ ok = $false; refusal = 'unsafe-parent-chain'; detail = "unknown op: $op" }
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
catch {
|
|
1216
|
+
[Console]::Error.WriteLine("op '$op' exception: $($_.Exception.Message)")
|
|
1217
|
+
Write-Frame @{ ok = $false; refusal = 'unsafe-parent-chain'; detail = "helper op exception" }
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
finally {
|
|
1222
|
+
try { [JaviForge.SecureObj]::CloseAll() } catch { }
|
|
1223
|
+
}
|