Symlink issues in ReFS on Windows

Publication date: 2026-06-03

Recently, I've hit a very peculiar case with ReFS on Windows (most often used as a "Windows Dev Drive"). Imagine that you have the following file system structure:

.
├─ Versions\
│  ├─ A\
│  │  └─ file.txt
│  └─ Current → A
└─ file.txt → Versions\Current\file.txt

In this diagram, an arrow means that the file is a symlink, and the path to the target is relative to the symlink's parent.

You would, of course, expect that reading from file.txt leads to reading from Versions\A\file.txt. This is sane and it works. Usually. Of course, in practice not everything is so simple.

In my case, it all started with an archive containing part of a Unix sysroot, and thus it was full of symlinks. I needed to unpack it on Windows, onto my DevDrive formatted as ReFS, and its unpacking led to a similar file system structure.

Imagine my confusion when I invoked cat file.txt and was met with a "file not found" response. The file is right there! Moreover, the symlink target is also right there, it exists!

So, as a reasonable person, I started investigating — I invoked commands ls Versions\A and ls Versions\Current to figure out the symlink structure. I looked at it again, it made sense. The file should be there, right? So, I invoked cat file.txt again, just to make sure that I didn't miss anything before.

And voilà — the command now worked!

What? How?

Let's investigate.

First of all, I've created a script to reproduce the issue. Run repro.ps1 -Disk W: (where W: is your DevDrive) and see its output. If it prints any warnings — it means your system also has this problem.

param (
    $Disk = "W:\",
    $Base = "$Disk\symlinks_1",
    $Base2 = "$Disk\symlinks_2"
)

New-Item -Type Directory -Path "$Base\Versions\A" -Force | Out-Null
Set-Content -Path "$Base\Versions\A\file.txt" -Value "This is file.txt"

cmd /c mklink "$Base\Versions\Current" "A"
cmd /c mklink "$Base\file.txt" "Versions\Current\file.txt"

Get-Content "$Base\file.txt"
if ($?) {
    Write-Host "Symlink 1 works correctly."
} else {
    Write-Host "Symlink 1 does not work."
}

Move-Item $Base $Base2
Get-Content "$Base2\file.txt" -ErrorAction SilentlyContinue
if ($?) {
    Write-Host "Symlink 2 works correctly."
} else {
    Write-Warning "Symlink 2 does not work."
    Write-Host "I will perform a fixup operation."

    Write-Host "Get-ChildItem $Base2"
    Get-ChildItem $Base2 | Out-Null

    Write-Host "Get-ChildItem $Base2\Versions"
    Get-ChildItem "$Base2\Versions" | Out-Null

    Write-Host "Get-ChildItem $Base2\Versions\Current"
    Get-ChildItem "$Base2\Versions\Current" | Out-Null

    Get-Content "$Base2\file.txt" -ErrorAction SilentlyContinue
    if ($?) {
        Write-Host "Symlink 2 works correctly after fixup."
    } else {
        Write-Error "Symlink 2 still does not work after fixup."
    }
}

My script creates this symlink structure, then renames the folder (to drop any FS caches) and tries to read the file. If the file is impossible to read — this is a bug in the FS implementation. The script then tries to execute several Get-ChildItem commands to demonstrate that they have a side effect on symlinks: after you run Get-ChildItem, the symlink gets "fixed" and works — until reboot, probably; I wasn't able to figure out the cache lifetime.

Note that this issue only reproduces on ReFS volumes. If you run it on NTFS, everything will be fine!

I asked a couple of people to run this script on their ReFS volumes, and indeed, the issue reproduces, at least on Windows 11 25H2.

What's going on? There's something missing from my script. Let's read the help for the mklink command:

$ cmd /c mklink /?
Creates a symbolic link.

MKLINK [[/D] | [/H] | [/J]] Link Target

        /D      Creates a directory symbolic link.  Default is a file
                symbolic link.
        […]

Note that my script never passes the /D flag to mklink, so technically it creates a file symlink. A file symlink to a directory!

What exactly could this mean?

First, let's figure out what the difference is. The dir command in cmd knows the difference: it marks a file symlink as <SYMLINK> and a directory symlink as <SYMLINKD>. Let's experiment:

$ cmd /c mklink a ..
$ cmd /c mklink /d b ..
$ cmd /c dir

<DIR>          .
<DIR>          ..
<SYMLINK>      a [..]
<SYMLINKD>     b [..]

(some output omitted).

Okay, what's the difference on the WinAPI level? See the documentation on the CreateSymbolicLinkW function: there's dwFlags = SYMBOLIC_LINK_FLAG_DIRECTORY, 0x1. This is the same flag that's called /D in the MKLINK command's description.

Unfortunately, it is not well documented. In fact, I was unable to find any documentation on the behavior of a file symlink when the target is a directory. Perhaps people never noticed the difference because it's absent on NTFS volumes?

Nevertheless, this behavior is problematic, and it's not very clear how to work around it in the general case. You can either resolve all your symlinks as a temporary solution, or adjust them so that directory symlinks are correctly marked as such. But there's no easy way, for example, to fix the unpacking software to automatically determine the correct flag when creating symlinks on disk: in general, the symlink target isn't guaranteed to exist at the moment the symlink is created, so you might not even know what kind of target you'll need. And of course, Unix-originated archive formats don't preserve the directory bit for symlinks — there's no such thing on Unix (to the best of my knowledge).

As external artifacts of this investigation, I have the following: