Is there any way around it? yes, but undocumented and only in case IO_REPARSE_TAG_MOUNT_POINT (but not in case IO_REPARSE_TAG_SYMLINK)
first of all - why this happens ? this is done by NTFS in NtfsFindStartingNode procedure
NTFS uppercase file name during parsing in NtfsFindStartingNode for calculate hash
which than used in NtfsFindPrefixHashEntry. also if PrefixHashEntry found - NTFS revert file name component back,
by call memmove from NtfsFindPrefixHashEntry. so NTFS usual temporary uppercase file name and than revert it to original form
but in case reparse point found - uppercase whole name, but revert back only until reparse name.
part after reparse file name left in uppercase
in case say \Pool\HelloWorld\XxXx NTFS first convert name to \POOL\HELLOWORLD\XXXX, then revert back to \Pool\HELLOWORLD\XXXX
and return STATUS_REPARSE with IO_REPARSE_TAG_MOUNT_POINT in your case and return
Tail.Overlay.AuxiliaryBuffer will point to REPARSE_DATA_BUFFER
assume that in REPARSE_DATA_BUFFER will be \Pool -> \MyDevice[\SomePath]
based on this info IopParseDevice transorm file name and new Irp will be sendto \MyDevice device
new file name will be [\SomePath]\HELLOWORLD\XXXX
before win7 (or vista - i not check here) when you view request to [\SomePath]\HELLOWORLD\XXXX
you can not even know - are user direct open
\MyDevice[\SomePath]\HELLOWORLD\XXXX
or open via reparse
??\c:\Pool\HELLOWORLD\XXXX
but after extra create parameter (ECP) appeared, situation is changed.
begin how minimum win7 (i not check on vista) special IopSymlinkECPGuid exist in kernel:
struct __declspec(uuid(“73d5118a-88ba-439f-92f4-46d38952d250”)) IopSymlinkECPGuid;
and IopParseDevice attach ECP context of type IopSymlinkECPGuid to Irp after reparse.
so now we can know exactly, are create request was via reparse - need check for IopSymlinkECPGuid
PECP_LIST EcpList;
SYMLINK_ECP_CONTEXT* EcpContext;
if (0 <= FsRtlGetEcpListFromIrp(Irp, &EcpList) && EcpList &&
0 <= FsRtlFindExtraCreateParameter(EcpList, &__uuidof(IopSymlinkECPGuid), (void**)&EcpContext, 0)){
//…
}
the context point to next structure:
struct SYMLINK_ECP_CONTEXT
{
USHORT UnparsedNameLength;
union {
USHORT Flags;
struct {
USHORT MountPoint : 1;
};
};
USHORT DeviceNameLength;
USHORT Zero;
SYMLINK_ECP_CONTEXT* Reparsed;
UNICODE_STRING Name;
};
the Flags i not exactly research -
in case IO_REPARSE_TAG_MOUNT_POINT (a00000003) low bit is set to 1, Flags == 3
in case IO_REPARSE_TAG_SYMLINK (a000000c) Flags == 2
also exist special case for undocumented reparse tag 80000019
the Name always containing full path (including device name) to file
Zero always init to 0 (padding field)
DeviceNameLength containing length in bytes of device path in Name
in case IO_REPARSE_TAG_MOUNT_POINT:
Name containing original file path from open request
UnparsedNameLength containing length in bytes of unparsed suffix (path after mount pount component)
Reparsed point to linked SYMLINK_ECP_CONTEXT data with
Flags = 0, Name containing reparsed file path
in case IO_REPARSE_TAG_SYMLINK the UnparsedNameLength and Reparsed is always 0
and Name containing reparsed file path
so in case IO_REPARSE_TAG_SYMLINK SYMLINK_ECP_CONTEXT containing much less info compare IO_REPARSE_TAG_MOUNT_POINT.
let concrete example:
on \Device\HarddiskVolume2 exist mount point :
\Pool -> \MyDevice[\SomePath]
and somebody try open file \Device\HarddiskVolume2\Pool\HelloWorld\XxXx
----- Irp #1 to \Device\HarddiskVolume2 --------
FileName = \Pool\HelloWorld\XxXx
no IopSymlinkECPGuid context
NTFS transform FileName to \Pool\HELLOWORLD\XXXX and return
(STATUS_REPARSE, IO_REPARSE_TAG_MOUNT_POINT)
----- Irp #2 to \MyDevice --------
FileName = [\SomePath]\HELLOWORLD\XXXX
exist IopSymlinkECPGuid context (Flags = 3):
Name = \Device\HarddiskVolume2\Pool\HelloWorld\XxXx
DeviceNameLength = size of L"\Device\HarddiskVolume2" (not including terminated 0)
UnparsedNameLength = sizeof of L"\HelloWorld\XxXx" (not including terminated 0)
Reparsed -> IopSymlinkECPGuid context (Flags = 0):
Name = \MyDevice[\SomePath]\HELLOWORLD\XXXX
DeviceNameLength = size of L"\MyDevice"
UnparsedNameLength = 0
Reparsed = 0
so based on this info you can got original unparsed file name (in original case) - \HelloWorld\XxXx
and modify file name from [\SomePath]\HELLOWORLD\XXXX to \HelloWorld\XxXx
BOOLEAN RevertFileName(PIRP Irp)
{
PECP_LIST EcpList;
SYMLINK_ECP_CONTEXT* EcpContext;
if (0 <= FsRtlGetEcpListFromIrp(Irp, &EcpList) &&
EcpList &&
0 <= FsRtlFindExtraCreateParameter(EcpList, &__uuidof(IopSymlinkECPGuid), (void**)&EcpContext, 0) &&
!FsRtlIsEcpFromUserMode(EcpContext) &&
EcpContext->MountPoint
)
{
if (USHORT UnparsedNameLength = EcpContext->UnparsedNameLength)
{
PUNICODE_STRING FileName = &IoGetCurrentIrpStackLocation(Irp)->FileObject->FileName;
USHORT FileNameLength = FileName->Length;
USHORT NameLength = EcpContext->Name.Length;
if (UnparsedNameLength <= NameLength && UnparsedNameLength <= FileNameLength)
{
UNICODE_STRING us1 = {
UnparsedNameLength,
UnparsedNameLength,
(PWSTR)RtlOffsetToPointer(FileName->Buffer, FileNameLength - UnparsedNameLength)
};
UNICODE_STRING us2 = {
UnparsedNameLength,
UnparsedNameLength,
(PWSTR)RtlOffsetToPointer(EcpContext->Name.Buffer, NameLength - UnparsedNameLength)
};
if (RtlEqualUnicodeString(&us1, &us2, TRUE))
{
memcpy(us1.Buffer, us2.Buffer, UnparsedNameLength);
return TRUE;
}
}
}
}
return FALSE;
}
some logs:
https://i.imgur.com/fUUFRWM.png https://pastebin.com/nRpZrZtJ
https://i.imgur.com/W3ywGUw.png https://pastebin.com/kCvzMRyi
https://i.imgur.com/2k0dARs.png https://pastebin.com/geHM893X