Skip to content

NTFS Junctions should be serialised with SubsituteName instead of PrintName #340

@ShadowLNC

Description

@ShadowLNC

If a junction is created with New-Item in PowerShell, the PrintName won't be set. The SubstitueName is ignored when creating the container layer, and this results in a corrupt junction.

Steps to reproduce

(I'm using BuildKit, containerd, and nerdctl on Windows with an install process adapted from the BuildKit docs, but I suspect it would do the same thing on Docker too.)

# escape=`
FROM mcr.microsoft.com/windows/servercore:ltsc2025

WORKDIR C:\test
RUN mkdir tgt `
    && powershell -c "New-Item -ItemType Symboliclink -Path ps-sym -Target tgt" `
    && powershell -c "New-Item -ItemType Junction -Path ps-jun -Target tgt" `
    && mklink /j cmd-jun tgt `
    && mklink /d cmd-sym tgt `
    && powershell -c "Get-ChildItem | Where-Object { $_.LinkType } | ForEach-Object { echo \"QUERY FOR $($_.FullName)\"; fsutil reparsepoint query $_.FullName; echo '-------' }" `
    && dir
RUN powershell -c "Get-ChildItem | Where-Object { $_.LinkType } | ForEach-Object { echo \"QUERY FOR $($_.FullName)\"; fsutil reparsepoint query $_.FullName; echo '-------' }" `
    && dir
# Prevent actually building image
RUN exit 1

You will notice that there is no PrintName set (it's empty) in the PowerShell-created junction, whereas mklink in cmd does set it.
In the second RUN step, you can see that the SubstituteName is lost on the Powershell-created junction, but it should be retained.

Cause

According to Claude AI, here's the exact problematic code in reparse.go, the decodeWindowsReparsePointData function:

func decodeWindowsReparsePointData(b []byte, isMountPoint bool) (*ReparsePoint, error) {
    nameOffset := 8 + binary.LittleEndian.Uint16(b[4:6])   // ← PrintNameOffset
    if !isMountPoint {
        nameOffset += 4
    }
    nameLength := binary.LittleEndian.Uint16(b[6:8])       // ← PrintNameLength
    name := make([]uint16, nameLength/2)
    // ...
}

Based on the AI response, I'm assuming this is called by https://github.com/microsoft/hcsshim/ which is in turn used by containerd/BuildKit.

Other

I'm not actually sure if PrintName is meant to be optional or required. Clearly PowerShell isn't setting it, nor is it present in some Rust libraries (see astral-sh/uv#17966), but perhaps future versions of PowerShell should set it? I can raise an issue on the PowerShell repo, but I don't think v5 (which is the only version available in Windows containers) will receive any such fixes, so a workaround is probably still necessary here.

(Also in PowerShell 7, it will refuse to accept a relative path as the target for a junction in New-Item, rather than silently converting it, so you'd have to use the value (Get-Item tgt).FullName.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions