22#include " ../win/WinAPI.h"
33#include " ../win/Handle.h"
44#include " ../win/HrError.h"
5- #include < winternl.h >
5+ #include " ../win/Security.h "
66#include < winioctl.h>
7- #include < sddl.h>
87#include < aclapi.h>
98#include < vector>
109#include < filesystem>
1110#include " ../Exception.h"
1211#include " ../log/Log.h"
1312
14- #pragma comment(lib, "Advapi32.lib")
15- #pragma comment(lib, "Ntdll.lib")
16-
1713namespace fs = std::filesystem;
1814
1915namespace pmon ::util::file
2016{
2117 // internal helpers
2218 namespace
2319 {
20+ // open a directory into handle without following reparse at the path leaf
2421 win::Handle OpenDirNoFollow_ (const fs::path& dirPath,
25- DWORD desiredAccess = FILE_LIST_DIRECTORY | READ_CONTROL | WRITE_DAC | SYNCHRONIZE,
26- DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE )
22+ DWORD desiredAccess = FILE_LIST_DIRECTORY | READ_CONTROL | WRITE_DAC | SYNCHRONIZE | DELETE ,
23+ DWORD share = FILE_SHARE_READ | FILE_SHARE_WRITE)
2724 {
2825 auto h = (win::Handle)CreateFileW (
2926 dirPath.c_str (),
@@ -52,25 +49,20 @@ namespace pmon::util::file
5249 hdr.ReparseTag = tag;
5350 hdr.ReparseDataLength = 0 ;
5451 hdr.Reserved = 0 ;
55-
5652 DWORD bytes = 0 ;
57- BOOL ok = DeviceIoControl (
53+ return ( bool ) DeviceIoControl (
5854 h,
5955 FSCTL_DELETE_REPARSE_POINT,
6056 &hdr,
6157 sizeof (hdr),
6258 nullptr ,
6359 0 ,
6460 &bytes,
65- nullptr );
66-
67- if (!ok) {
68- pmlog_warn (" Could not delete reparse point by handle" ).hr ();
69- return false ;
70- }
71- return true ;
61+ nullptr
62+ );
7263 }
7364
65+ // check if a directory referenced by the handle has a reparse point
7466 std::optional<DWORD> IsReparseByHandle_ (HANDLE h)
7567 {
7668 FILE_ATTRIBUTE_TAG_INFO tag{};
@@ -85,142 +77,101 @@ namespace pmon::util::file
8577 }
8678 }
8779
88- // Create/open a directory named 'leafName' as a child of 'parent' without following reparses.
80+ // Create/open a directory named 'leafName' as a child of 'parent'
81+ // If running elevated, additionally undertake the following defensive measures:
8982 // If the entry is a reparse point, attempt to convert it in place by deleting its reparse
9083 // attribute; if that fails, delete and recreate as a real directory.
91- // Apply SYSTEM-only DACL by handle if isElevated == true.
92- fs::path CreateOrOpenDirSystemOnlySecure_ (const fs::path& parent,
84+ win::Handle CreateOrOpenDirSystemOnlySecure_ (const fs::path& parent,
9385 const std::wstring& leafName,
9486 bool isElevated)
9587 {
9688 if (leafName.find_first_of (L" /\\ " ) != std::wstring::npos) {
9789 throw Except<Exception>(" leafName must not contain path separators" );
9890 }
9991
100- // 1) Open parent directory by handle (no-follow) and verify/normalize reparse if present.
101- DWORD parentAccess = FILE_LIST_DIRECTORY | SYNCHRONIZE | READ_CONTROL;
102- if (isElevated) parentAccess |= WRITE_DAC;
92+ const auto fullpath = parent / leafName;
10393
104- auto hParent = (win::Handle)OpenDirNoFollow_ (parent, parentAccess);
105- if (!hParent) {
106- throw Except<win::HrError>(" Open parent directory failed" );
94+ // setup ACL to apply
95+ UniqueLocalPtr<void > pSecDesc;
96+ if (isElevated) {
97+ pSecDesc = win::MakeSecurityDescriptor (" D:P(A;OICI;FA;;;SY)" );
10798 }
108-
109- if (auto reparseTag = IsReparseByHandle_ (hParent)) {
110- // Try to delete the reparse attribute on the parent (best-effort).
111- if (!TryDeleteReparsePointByHandle_ (hParent, *reparseTag)) {
112- throw Except<Exception>(" Parent directory is a reparse point and could not be normalized" );
99+ SECURITY_ATTRIBUTES secAttr{
100+ .nLength = sizeof (SECURITY_ATTRIBUTES),
101+ .lpSecurityDescriptor = pSecDesc.get (),
102+ };
103+
104+ // 1. try and create with ACL
105+ const auto CreateNew = [&] {
106+ if (CreateDirectoryW (fullpath.c_str (), &secAttr)) {
107+ // successfully created new directory
108+ pmlog_dbg (" Created new temp subdir" ).pmwatch (fullpath.string ());
109+ return true ;
113110 }
114- // Re-check; if still reparse, bail.
115- if (IsReparseByHandle_ (hParent)) {
116- throw Except<Exception>(" Parent directory remains a reparse point after normalization" );
111+ else if (HRESULT hr = GetLastError (); hr != ERROR_ALREADY_EXISTS) {
112+ throw Except<win::HrError>(hr, " Failed to create secure subdir" );
117113 }
118- }
119-
120- // 2) Create/open the child directory *by name relative to parent handle* via NtCreateFile.
121- UNICODE_STRING uName{};
122- uName.Buffer = const_cast <wchar_t *>(leafName.c_str ());
123- uName.Length = static_cast <USHORT>(leafName.size () * sizeof (WCHAR));
124- uName.MaximumLength = uName.Length ;
125-
126- OBJECT_ATTRIBUTES oa{};
127- InitializeObjectAttributes (&oa, &uName, OBJ_CASE_INSENSITIVE, hParent, nullptr );
128-
129- IO_STATUS_BLOCK ios{};
130- win::Handle hChild;
114+ return false ;
115+ };
116+ const auto isFresh = CreateNew ();
131117
132- ACCESS_MASK childAccess = FILE_LIST_DIRECTORY | SYNCHRONIZE | READ_CONTROL | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES;
133- if (isElevated) childAccess |= WRITE_DAC;
134- NTSTATUS st = NtCreateFile (
135- hChild.ClearAndGetAddressOf (),
136- childAccess,
137- &oa,
138- &ios,
139- nullptr , // AllocationSize
140- FILE_ATTRIBUTE_DIRECTORY,
141- FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
142- FILE_OPEN_IF, // create if missing, open if exists
143- FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, // directory open/create
144- nullptr ,
145- 0 );
146- if (st < 0 ) {
147- throw Except<Exception>(std::format (" NtCreateFile(directory) failed; NTSTATUS:{}" , st));
118+ // 2. open the existing dir without following any reparse on it
119+ auto hExistingSubdir = OpenDirNoFollow_ (fullpath);
120+ if (!hExistingSubdir) {
121+ throw Except<win::HrError>(" Failed to open existing subdir by handle" );
148122 }
149123
150- // 3) If the child is a reparse point, attempt to normalize it.
151- if (auto reparseTag = IsReparseByHandle_ (hChild)) {
152- if (bool normalized = TryDeleteReparsePointByHandle_ (hChild, *reparseTag); !normalized) {
153- // Close the handle and attempt delete-and-recreate as a plain directory.
154- hChild.Clear ();
155-
156- fs::path full = fs::path (parent) / leafName;
157- if (!RemoveDirectoryW (full.c_str ())) {
158- pmlog_warn (" Failed remove reparse as dir" ).hr ();
159- // As a last-ditch attempt, if it's a file reparse, try DeleteFileW
160- if (!DeleteFileW (full.c_str ())) {
161- pmlog_error (" Could not remove reparse point" ).hr ();
162- }
124+ // if dir is not fresh and we are elevated, we have defensive work to do
125+ if (!isFresh && isElevated) {
126+ // 3. own the existing dir
127+ {
128+ // get the dacl out of the security description
129+ PACL pDacl = nullptr ;
130+ BOOL daclPresent = FALSE , daclDefaulted = FALSE ;
131+ if (!GetSecurityDescriptorDacl (pSecDesc.get (), &daclPresent, &pDacl, &daclDefaulted)) {
132+ throw Except<win::HrError>(" GetSecurityDescriptorDacl failed" );
163133 }
164-
165- // Recreate with FILE_CREATE to ensure a brand-new real directory
166- st = NtCreateFile (
167- hChild.ClearAndGetAddressOf (),
168- childAccess,
169- &oa,
170- &ios,
171- nullptr ,
172- FILE_ATTRIBUTE_DIRECTORY,
173- FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
174- FILE_CREATE,
175- FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT,
176- nullptr ,
177- 0 );
178- if (st < 0 ) {
179- throw Except<Exception>(std::format (
180- " Recreate plain directory replacing reparse point failed; NTSTATUS:{}" , st));
134+ // set security on the directory object
135+ if (auto rc = SetSecurityInfo (
136+ hExistingSubdir,
137+ SE_FILE_OBJECT,
138+ DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
139+ nullptr , // owner
140+ nullptr , // group
141+ pDacl,
142+ nullptr // sacl
143+ ); rc != ERROR_SUCCESS) {
144+ throw Except<win::HrError>(HRESULT (rc), " SetSecurityInfo(DACL) failed" );
181145 }
182146 }
183- // After dealing with reparse point, confirm it's now plain.
184- if (IsReparseByHandle_ (hChild)) {
185- throw Except<Exception>(" Directory remains a reparse point after cleansing efforts" );
186- }
187- }
188-
189- // 4) Apply SYSTEM-only DACL by handle (no path) - only if running elevated.
190- if (isElevated) {
191- // SDDL: D:P(A;OICI;FA;;;SY) -> protected DACL, SYSTEM full, inherits to children.
192- LPCWSTR sddl = L" D:P(A;OICI;FA;;;SY)" ;
193- PSECURITY_DESCRIPTOR psd = nullptr ;
194- if (!ConvertStringSecurityDescriptorToSecurityDescriptorW (sddl, SDDL_REVISION_1, &psd, nullptr )) {
195- throw Except<win::HrError>(" ConvertStringSecurityDescriptorToSecurityDescriptorW failed" );
196- }
197-
198- PACL dacl = nullptr ;
199- BOOL daclPresent = FALSE , daclDefaulted = FALSE ;
200- if (!GetSecurityDescriptorDacl (psd, &daclPresent, &dacl, &daclDefaulted)) {
201- ::LocalFree (psd);
202- throw Except<win::HrError>(" GetSecurityDescriptorDacl failed" );
203- }
204147
205- DWORD rc = SetSecurityInfo (
206- hChild,
207- SE_FILE_OBJECT,
208- DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
209- nullptr , // owner
210- nullptr , // group
211- dacl,
212- nullptr // sacl
213- );
214-
215- ::LocalFree (psd);
216-
217- if (rc != ERROR_SUCCESS) {
218- throw Except<win::HrError>((HRESULT)rc, " SetSecurityInfo(DACL) failed" );
148+ // 3. check for reparse
149+ if (auto tag = IsReparseByHandle_ (hExistingSubdir)) {
150+ pmlog_warn (" detected reparse point when estabilishing subdir" ).pmwatch (fullpath.string ());
151+ // 3a. try and remove reparse point
152+ if (TryDeleteReparsePointByHandle_ (hExistingSubdir, *tag)) {
153+ pmlog_dbg (" deleted reparse point from subdir" );
154+ }
155+ else {
156+ pmlog_warn (" Failed to delete reparse point from subdir" );
157+ // 3b. try to delete entire directory
158+ hExistingSubdir.Clear ();
159+ fs::remove_all (fullpath);
160+ // 3c. try and create anew with ACL
161+ if (!CreateNew ()) {
162+ throw Except<Exception>(" Failed to create new directory after deleting existing" );
163+ }
164+ hExistingSubdir = OpenDirNoFollow_ (fullpath);
165+ }
166+ // 3d. final check that directory no longer has reparse
167+ if (IsReparseByHandle_ (hExistingSubdir)) {
168+ throw Except<Exception>(" Could not neutralize reparse obstacle" );
169+ }
219170 }
220171 }
221172
222- // 5) Return the final absolute path for convenience.
223- return parent / leafName ;
173+ // we are in the clear
174+ return hExistingSubdir ;
224175 }
225176 }
226177
@@ -230,14 +181,11 @@ namespace pmon::util::file
230181 bool deleteOnDestruct,
231182 bool clearOnConstruct)
232183 {
233- if (name.empty ()) {
234- throw Except<Exception>(" name must not be empty" );
235- }
236-
237184 SecureSubdirectory d;
238185 d.isElevated_ = isElevated;
239186 d.deleteOnDestruct_ = deleteOnDestruct;
240- d.path_ = CreateOrOpenDirSystemOnlySecure_ (parent, name, isElevated);
187+ d.path_ = parent / name;
188+ d.hDirectory_ = CreateOrOpenDirSystemOnlySecure_ (parent, name, isElevated);
241189
242190 if (clearOnConstruct) {
243191 d.Clear ();
@@ -270,7 +218,8 @@ namespace pmon::util::file
270218 :
271219 path_ (std::move(other.path_)),
272220 deleteOnDestruct_ (other.deleteOnDestruct_),
273- isElevated_ (other.isElevated_)
221+ isElevated_ (other.isElevated_),
222+ hDirectory_ (std::move(other.hDirectory_))
274223 {
275224 other.deleteOnDestruct_ = false ;
276225 other.path_ .clear ();
@@ -291,6 +240,7 @@ namespace pmon::util::file
291240 path_ = std::move (other.path_ );
292241 deleteOnDestruct_ = other.deleteOnDestruct_ ;
293242 isElevated_ = other.isElevated_ ;
243+ hDirectory_ = std::move (other.hDirectory_ );
294244 other.deleteOnDestruct_ = false ;
295245 other.path_ .clear ();
296246 other.isElevated_ = false ;
0 commit comments