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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions