34

In Windows (assume 2000 onwards), a file path can be at most approximately 32767 characters in length. This limitation exists due to the internal handling with UNICODE_STRING in the native API (also on the kernel side, in drivers etc). So far so good. I know the theory behind that part.

The reason for the limit is that the the Length and MaximumLength members of UNICODE_STRING count the number of bytes in the Buffer, but are 16 bit unsigned integers themselves.

I also know why the limit is an approximation rather than a set limit. This is mostly due to how your file name (e.g. \\.\C:\boot.ini) gets resolved to its native form (e.g. \??\C:\boot.ini) and then to something that is prefixed by the actual volume device name and then followed by the path relative to that volume, e.g. \Device\HarddiskVolume2\boot.ini.

Furthermore from Windows Explorer the known symptom when hitting the ("ANSI") MAX_PATH limit is to pretend the file or folder doesn't exist in some versions of Windows (possible this got fixed at some point).

But what happens at the object manager, I/O manager and file system driver levels respectively when I call CreateFile() with a path that looks like \\.\C:\...\filename.ext and the whole path does not exceed the limit, but reaches it, in my call to kernel32.dll's CreateFile() and then gets expanded? ...

Neither the SDKs nor the WDKs seem to be particularly chatty about the topic. Or did I look in the wrong sections?

0xC0000022L
  • 20,597
  • 9
  • 86
  • 152
  • I also think this is an interesting question, but A) nobody outside of a Microsoft kernel developer could definitively answer it and B) the answer wouldn't be particularly useful anyway. – Luke Mar 07 '13 at 15:05
  • 1
    @Luke: being a bug hunter I find every little detail not just intriguing but often end up finding them useful in hindsight when tracking down issues. For example when I see particular bug-checks on Windows my experience tells me already where to look first. This will hopefully be another such jigsaw piece :) – 0xC0000022L Mar 07 '13 at 16:56
  • You could try looking at Wine or ReactOS source code for an idea, though there's no guarantee it's exactly the same as what Windows does (but it is likely since they are striving for compatibility). Or you could just debug it and find out for yourself, though this will probably require kernel debugging. – Luke Mar 08 '13 at 13:43
  • This question assumes that path expansion happens in kernel mode; from what I can tell it does not (though I am not a kernel developer and thus not confident enough to post this as an answer). See [here](http://www.osronline.com/article.cfm?article=17#Q5) and [here](http://www.osronline.com/article.cfm?article=381) for some explanation of how it works. It seems to me the expansion happens inside CreateFile(), which fails gracefully if it crosses the 32k UNICODE_STRING length limit; Object Manager et al only parse the names. – Luke Mar 11 '13 at 17:48
  • @Luke: I am a kernel developer and I know that parts of the expansion happens in kernel mode. You'll notice in my question that I point out various stages at which I know an expansion will happen. For example in my file system filters I'll commonly see all the symbolic links (object manager lingo, not to be confused with the FS entities) resolved (so instead of `\??\C:` I get to see `\Device\HarddiskVolume2`). But what `CreateFile` does is only to translate the Win32 path to the native one and passing it on (verified through IDA). I.e. `C:` (ANSI/MBCS) or `\\.\C:` (Unicode) to `\??\C:`. – 0xC0000022L Mar 11 '13 at 17:56
  • 1
    _"what happens [internally] when I call [a public API with parameters I know the API's internals will fiddle with, causing errors]?"_ - while this is an interesting question, I think the answer is _"That's an implementation detail, bound to change between versions, just be sure to check the return value of the function."_ – CodeCaster Mar 12 '13 at 09:07
  • 6
    This sounds like a job for @RaymondChen. – Vsevolod Golovanov Mar 12 '13 at 12:40
  • 1
    Long file names are supported since NTFS 1.x and 32767 (+/-) characters can also be used on Windows NT 3.1 already – Thomas Weller Jan 18 '14 at 19:28

1 Answers1

41

Because I'm lazy, I didn't write a test program but tested it using the excellent Far Manager which handles things like long paths (longer than MAX_PATH) or special filenames (con, prn etc) just fine.

I made a string of exactly 255 characters ("12345678901234...012345") and started creating nested directories. Luckily, Far's "Make Directory" function takes a slash-separated string to mean "create nested directories" so I was able to do it in just a few steps by preparing a string in the internal editor with some copy&paste.

The longest path I was able to create was 32739 characters long, counting from "C:\" (i.e. it does not include "\\?\" added by Far). The error that I get when trying to create a directory or file with just one additional character is "The filename or extension is too long.". If I try to enter that directory, I get the same error.

EDIT: spent some time in the debugger and here's what happens on the Win32 API level:

  1. I try to create a file with one character above the limit
  2. Far calls CreateFileW with the string "\\?\C:\123[...]012345" which is 32744 wide characters long (not counting the terminating zero).
  3. CreateFileW does some extra checks, converts the null-terminated string to UNICODE_STRING (Length=65488, MaximumLength=65490) and prepares an OBJECT_ATTRIBUTES struct.
  4. CreateFileW then calls NtCreateFile in ntdll.dll, which is just a wrapper around syscall instruction.
  5. NtCreateFile returns 0xC0000106 (STATUS_NAME_TOO_LONG).
  6. That status value is then converted (using RtlNtStatusToDosError) to the Win32 error 206 (ERROR_FILENAME_EXCED_RANGE).

I did not bother checking what happens in the kernel, but I guess I could have a look at that too.

EDIT2: I ran WinObj and found that on my system C: is a symlink to \Device\HarddiskVolume1. This string is 23 characters long. If we replace the \C: in the string passed to NtCreateFile with it, we get 32744 - 3 + 23 = 32764 characters. Together with the terminating zero, this requires 65530 bytes. Still short of the limit (0xFFFF=65535) so I guess there's something extra being added, like a session or namespace name.

EDIT3: after going through the kernel:

  1. NtCreateFile calls IopCreateFile
  2. IopCreateFile calls ObOpenObjectByName
  3. ObOpenObjectByName calls ObpLookupObjectName
  4. ObpLookupObjectName checks for ObpDosDevicesShortNamePrefix ("\??\") -> success
  5. it skips the prefix and splits the remaining part into "C:" and "\1234..."
  6. it resolves the "C:" with a call to ObpLookupDirectoryEntry
  7. it then calls ObpParseSymbolicLink passing to it the looked-up directory entry (_OBJECT_SYMBOLIC_LINK with LinkTarget == "\Device\HarddiskVolume1" and DosDeviceDriveIndex == 3) and the remaining part of the name.
  8. It then does something like this (faithfully reproduced by ReactOS):

    TargetPath = &SymlinkObject->LinkTarget;
    TempLength = TargetPath->Length;
    TotalLength = TempLength + RemainingName->Length;
    if (LengthUsed > 0xFFF0)
        return STATUS_NAME_TOO_LONG;
    

    In our case, 46 + 65476 = 65522 (0xfff2) which is just above the limit.

    So there, mystery solved (I hope!).

P.S. everything tested under Windows 7 x64 SP1.

Igor Skochinsky
  • 24,629
  • 2
  • 72
  • 109
  • 3
    "C: is a symlink to \Device\HarddiskVolume1", hah, so Windows is a Unix in disguise. – Siyuan Ren May 06 '14 at 12:18
  • @SiyuanRen: not quite. Or are you implying that a single-root namespace is something that only Unix has ever invented (and invented first)? – 0xC0000022L Mar 29 '16 at 19:34
  • @SiyuanRen: in particular it should be noted that the defining part in Unix is not so much the single root, but the "everything is a file" paradigm. However, that does not apply to NT. – 0xC0000022L Jun 15 '16 at 07:08