Windows user-mode NtCreateFile: reparse rejection, directory-handle namespace stability, and safe post-create guard handoff

Mesut Topal 0 Reputation points
2026-11-11T22:23:14.46+00:00

Windows user-mode NtCreateFile: reparse rejection, directory-handle namespace stability, and safe post-create guard handoff

We seek an authoritative, version-qualified support contract for local desktop directory creation and namespace confinement. Measurements used Windows 11 Home 25H2 x64, build 26200.9974, local NTFS and unelevated user-mode processes. Our goal is to create one new child within an authorized directory tree without following a substituted reparse entry or silently following a directory that has moved outside that tree.

Exact requests

Existing parent: NtCreateFile, DesiredAccess=0x00100081 (FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | SYNCHRONIZE), ShareAccess=3, FILE_OPEN=1, CreateOptions=0x21. Root/ancestor metadata handles in the experiment used DesiredAccess=0x80, ShareAccess=3. Proposed root/ancestor guards using 0x00100081/share3 are unvalidated proposals, not experimental results.

New child:


NtCreateFile(

  &originalDirectoryHandle, 0x00120080,

  &ObjectAttributes, &IoStatusBlock, NULL,

  FILE_ATTRIBUTE_NORMAL /* 0x80 */, FILE_SHARE_READ /* 1 */,

  FILE_CREATE /* 2 */,

  FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT /* 0x21 */,

  NULL, 0

)

ObjectAttributes.RootDirectory = held, validated filesystem-directory handle

ObjectAttributes.ObjectName = one literal relative component: candidate-directory

ObjectAttributes.Attributes = 0x1040 /* OBJ_CASE_INSENSITIVE | OBJ_DONT_REPARSE */

ObjectAttributes.SecurityDescriptor = NULL

ObjectAttributes.SecurityQualityOfService = NULL

0x00120080 is SYNCHRONIZE | READ_CONTROL | FILE_READ_ATTRIBUTES. We do not add FILE_OPEN_REPARSE_POINT or use a fallback that removes OBJ_DONT_REPARSE.

Three contract questions

1. PO-01 — Namespace continuity. For directories opened with 0x00100081/share3 and a full held ancestor chain, under precisely which conditions are parent rename, ancestor rename and moving a directory to another parent prevented throughout the handle lifetime? Please address prior DELETE-capable opens, FILE_DELETE_CHILD on the containing parent, in-place reparse changes, namespace aliases, filesystem/filter differences and supported Windows versions. Contrast these guards with 0x80 metadata-only handles; we do not infer equivalent protection. If no such continuity contract exists, what supported API or architecture provides it?

2. PO-02 — Reparse rejection before mutation. Is the exact user-mode FILE_CREATE directory request above supported and documented? How should the general OBJ_DONT_REPARSE definition be reconciled with NtCreateFile's narrower Attributes description? For a final-component junction, symlink or other reparse entry, including insertion between vacancy observation and syscall, is failure guaranteed before following the target and before the first filesystem mutation outside the authorized parent or modification of a foreign object? Please specify collision/status/completion behavior, required parent rights and authorization checks, and NTFS/tag/filter/version and packaged full-trust limitations. If unsupported or unspecified, what documented alternative should be used?

3. PO-03 — Original creation handle to lasting guard. What rename/move protection does the original 0x00120080/share1 directory handle provide? While retaining it, would acquiring an additional 0x00100081/share3 existing-directory handle provide a supported, continuous handoff? What identity, namespace and ordering preconditions are required? How can movement between creation and guard acquisition be prevented before the first child mutation? Please identify a documented creation/access/handle strategy if these masks cannot provide that property. Post-hoc File ID/path checks cannot undo an earlier unauthorized mutation.

Minimal retained observations

In the two-actor fixtures, actor A held the parent and observed an empty leaf; an independent actor B performed the planned namespace change and exited before A's target request. Deterministic ordering and before/after identity inventories were retained; this was not a high-frequency race flood.

  • Unpackaged candidate: 12 ordinary requests (4 new-directory successes, 8 collisions) and 6 final-leaf reparse/insertion/two-actor requests. All 6 reparse cases returned 0xC000050B; candidate outside-held-parent creations=0.
  • Separate negative control omitted OBJ_DONT_REPARSE (Attributes=0x40): its dangling-junction case created one synthetic object outside the held parent. It was not a candidate failure and is not a proposed safe configuration.
  • Separate package identity: 12 full-trust medium-integrity requests, including 6 reparse rejections, plus 12 distinct unpackaged baseline requests. Outside-held-parent creations=0. Deployment was unsigned loose development registration using pre-existing Developer Mode; signed-MSIX installation/distribution was not tested.
  • Separate parent-drift study: parent share3 rename failed with Win32 32. With share7 in distinct diagnostic fixtures, the same File ID moved outside the originally authorized subtree. The old pathname could name a different object. The normal policy denied create after drift; one explicit diagnostic create still used the moved held parent: outside-held-parent=0, outside-originally-authorized-subtree=1.
  • Ancestor rename attempts returned Win32 5; the proposed replacement junction was not installed. Those replacement cases remain NOT_TESTED.
  • Three separate CreateDirectory2W observations with DIRECTORY_FLAGS_DISALLOW_PATH_REDIRECTS=1 showed one new-directory handle and two junction-name failures (Win32 183), without observed outside-parent creation. This pathname API was not treated as equivalent to a held-RootDirectory request.

Limits and requested clarification

These results establish only the measured fixtures/build. They do not prove a universal first-mutation ordering, continuous namespace confinement, every reparse tag/filter/version, cross-volume safety, power-loss durability, or a safe original-handle-to-guard handoff. Parent drift is not presented as an OBJ_DONT_REPARSE defect. Native identity and original pathname authority are distinct.

Please classify each PO item as Supported, Unsupported, or Conditional, identify the exact preconditions and exclusions, and cite an applicable published contract or attributable Windows/documentation-owner clarification. Unspecified behavior is a useful answer; we will keep the corresponding write path disabled until resolved.

Relevant documentation:

Windows development | Windows API - Win32
0 comments No comments

2 answers

Sort by: Most helpful
  1. Gatlin Le (WICLOUD CORPORATION) 1,470 Reputation points Microsoft External Staff Moderator
    2026-11-12T06:04:44.6266667+00:00

    Hi @Mesut Topal ,

    Two of your three items can be answered more concretely from the Windows file system algorithms specification than from the API reference pages, because the protection you measured comes from sharing and rename rules rather than from a directory handle contract. I also ran the relevant cases locally, and the results follow those rules.

    [MS-FSA] describes the object store behavior that Windows file systems implement. Its sharing check for an existing stream or directory runs only when the new open requests FILE_READ_DATA, FILE_EXECUTE, FILE_WRITE_DATA, FILE_APPEND_DATA or DELETE, and an existing open counts only if its own granted access contains one of those bits. The dwShareMode description in CreateFileW, which you already listed as D3, says the same from the caller side: access requests to attributes are not affected by the share mode, and delete access covers both delete and rename. FileRenameInformation fails with STATUS_ACCESS_DENIED unless the open was granted DELETE, and also fails with STATUS_ACCESS_DENIED when the directory being renamed has open files anywhere beneath it.

    For namespace continuity (PO-01), I would classify it as Conditional. A held handle prevents rename or move of the directory it names only when it carries one of those data access bits and omits FILE_SHARE_DELETE. 0x00100081 qualifies through FILE_LIST_DIRECTORY, which uses the same bit as FILE_READ_DATA. 0x80 does not, so a metadata-only handle gives no rename protection to its own directory. That matches your Win32 32 with share3 and the successful move with share7. Ancestors are covered by the second rule instead: any open beneath an ancestor, including a 0x80 handle, makes renaming that ancestor fail, which matches your Win32 5.

    The other conditions you listed follow from the same rules. FILE_DELETE_CHILD on the containing parent can grant DELETE during the access check, but the sharing check still runs afterwards, so it should not bypass the guard. If a DELETE-capable open already exists, your guard open fails with a sharing violation, so acquisition fails closed, although that says nothing about where the directory was before the guard existed. Sharing state is tracked per file rather than per name, so an 8.3 short name should reach the same check. I did not test the FILE_DELETE_CHILD or 8.3 cases.

    Two limits matter here. [MS-FSA] is a protocol specification, not a version-qualified Win32 API contract, and its rename rule for open files beneath a directory carries product behavior notes. Kernel-mode callers can also pass IO_IGNORE_SHARE_ACCESS_CHECK to IoCreateFileEx, which tells the I/O manager to skip its share-access check, although the documentation notes the file system might still perform it.

    As for the guard handoff (PO-03), I would also classify it as Conditional. The original 0x00120080 handle contains no data access bits, so under the sharing rule above it never takes part in a sharing check. Its share1 mode has no effect, and another caller with DELETE permission can open and move the new directory while you hold that handle. A handoff to a second handle cannot close that gap, because the directory may already have moved by the time the second open runs. A simpler design is to request the guard access in the creating call itself, for example DesiredAccess 0x00120081 with a share mode that omits FILE_SHARE_DELETE. The creating handle then acts as the guard from the moment the directory exists, and no handoff is needed.

    To check these points, I ran two separate x64 processes on local NTFS, Windows build 26300.9457, which differs from your build. While the original 0x00120080/share1 handle stayed open, the second process opened the new child for DELETE and moved it outside the parent, and the creating handle then reported the new path with the same volume serial and File ID. With 0x00120081/share1 at creation, that DELETE open failed with Win32 32, and with 0x00120081/share7 the move succeeded again. A parent held with 0x80/share3 could itself be moved, while 0x00100081/share3 blocked the DELETE open with Win32 32. Holding only a 0x80 handle on the parent made renaming its ancestor fail with Win32 5. When the other process already held a DELETE handle with share7, opening a 0x00100081/share3 guard returned 0xC0000043 (STATUS_SHARING_VIOLATION), while the same open with share7 succeeded. After the blocking handles were closed, every blocked rename or guard open succeeded, which rules out permissions or destination problems. Both processes used restricted medium-integrity tokens with the Administrators group set to deny-only, not a separate standard-user logon. These results confirm the sharing and rename behavior on this configuration only. They do not cover an atomic handoff, first-mutation ordering, your build or other Windows versions.

    For reparse rejection (PO-02), I found no published statement covering packaged full-trust processes, but two documented points may help alongside OBJ_DONT_REPARSE. The RedirectionGuard mitigation, enabled through SetProcessMitigationPolicy, prevents the process from following file system junctions created by non-admin users. It can act as a process-wide backstop, but it covers junctions only. Your fixtures also ran with Developer Mode enabled, and in that mode CreateSymbolicLinkW lets non-elevated processes create symbolic links with SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE. Whether symbolic links belong in your threat model on customer machines therefore depends partly on that setting. I did not test either point in this round.

    Everything above is what I could observe and collect within this Q&A thread, from the published specifications and API references I could find and from my own local tests on one configuration. That is the limit of what I can confirm here, and I cannot give an official support statement on behalf of the Windows team. If you need a version-qualified statement, or confirmation of the first-mutation ordering and filter coverage, Windows Developer Support, under technical support for app platforms and Windows SDK, is the route for that.

    I hope the test results and documentation above help explain the behavior and give you a clear way to troubleshoot the issue. If you found my response helpful or informative, I would greatly appreciate it if you could follow this guidance so that others in the community facing a similar issue can find it more easily and help each other.

    Thank you.

    Was this answer helpful?

    0 comments No comments

  2. AI answer

    2026-11-11T22:23:48.5+00:00
    • PO-01 — Namespace continuity: Unsupported The available public documentation does not establish a version-qualified contract that a held directory handle keeps the directory continuously confined to the same parent or ancestor chain for the handle lifetime. The documented relative-name rule is only that when RootDirectory is non-null, the name is interpreted relative to that handle. The available material does not define a guarantee covering parent rename, ancestor rename, move to another parent, aliasing, in-place reparse changes, filesystem filters, or a supported Windows-version matrix for continuous namespace stability. The available material also does not establish that 0x00100081/share3 and 0x80/share3 provide equivalent rename or move protection. No supported API or architecture providing the requested continuity guarantee is identified in the available material.
    • PO-02 — Reparse rejection before mutation: Conditional The available material supports only these points:
      1. OBJECT_ATTRIBUTES defines OBJ_DONT_REPARSE as a flag meaning no reparse points are followed when parsing the name of the associated object.
      2. Public documentation for NtCreateFile does not expressly document the requested user-mode, directory-relative OBJ_DONT_REPARSE contract with the version, filesystem, filter, and mutation-order guarantees requested.
      3. Public documentation does not define an atomic object/effect boundary for the exact FILE_CREATE directory request relative to concurrent reparse insertion or conversion.
      The available material therefore does not establish that, for a final-component junction, symlink, or other reparse point inserted before or during the call, failure is guaranteed before any foreign mutation in every supported environment. It also does not establish the opposite. The supported conclusion is that the requested pre-mutation guarantee is unspecified by the available public documentation. On the documentation mismatch, the available material supports only that the NtCreateFile page has narrower wording for Attributes, while the OBJECT_ATTRIBUTES page gives the general definition of OBJ_DONT_REPARSE. The available material does not provide an official reconciliation beyond that documentation gap. As a pathname alternative, CreateDirectory2W documents DIRECTORY_FLAGS_DISALLOW_PATH_REDIRECTS, which prevents lpPathName from being redirected by reparse points or symbolic links. That is a documented pathname API behavior, but the available material does not state that it is equivalent to a held-RootDirectory relative create or that it provides the requested confinement guarantee.
    • PO-03 — Original creation handle to lasting guard: Unsupported The available material does not define what rename or move protection is provided by the original 0x00120080/share1 directory handle, and it does not define a supported continuous handoff from that handle to an additional 0x00100081/share3 existing-directory handle. The available material also does not define the identity, namespace, or ordering preconditions needed to guarantee that movement cannot occur between creation and guard acquisition before a first child mutation. Post-create identity checks may help detect drift, but the available material does not establish them as a way to prevent an earlier unauthorized mutation. On parent rights for relative opens, the available material supports only that the public contract does not specify which granted-access bits, if any, are required on the RootDirectory handle beyond the general relative-name behavior. A measured Q&A answer reports success with minimal parent-handle rights on one tested configuration, but that is not a published contract for all supported environments.

    In short:

    • PO-01: Unsupported
    • PO-02: Conditional — OBJ_DONT_REPARSE is generally defined, but the exact pre-mutation guarantee for the requested user-mode relative NtCreateFile(FILE_CREATE) directory call is not established by the available public documentation.
    • PO-03: Unsupported

    References:

    AI-generated content may be incorrect. Read our transparency notes for more information.

    Was this answer helpful?

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.