Author SHA1 Message Date
RandyTheSilly 99e90c1cba Apply patch 2026-02-09 22:38:30 -05:00
AdityaMittal1306 75610162e7 Merge pull request #336 from vagokuln-msft/fix-lx-symlink-support
Add support for LX symlinks (0xA000001D) for WSL/MSYS2 compatibility
2026-01-07 20:24:00 +05:30
Varun Gokulnath 164281c34b Addressed Feedback
Signed-off-by: Varun Gokulnath <gvarun22@outlook.com>
2025-12-17 11:16:27 -08:00
Varun Gokulnath a83edf96d5 Fix linting: remove extra empty line
Signed-off-by: Varun Gokulnath <vagokuln@microsoft.com>
Signed-off-by: Varun Gokulnath <gvarun22@outlook.com>
2025-12-17 11:16:26 -08:00
Varun Gokulnath 4efa80daf1 Refactor LX symlink logic into dedicated functions
Extract LX symlink encode/decode into separate functions for better
maintainability and cleaner separation of concerns.

Signed-off-by: Varun Gokulnath <vagokuln@microsoft.com>
Signed-off-by: Varun Gokulnath <gvarun22@outlook.com>
2025-12-17 11:16:26 -08:00
Varun Gokulnath 3ffe560692 Fix linting issues: remove redundant build tags and use errors.New
Signed-off-by: Varun Gokulnath <vagokuln@microsoft.com>
Signed-off-by: Varun Gokulnath <gvarun22@outlook.com>
2025-12-17 11:16:26 -08:00
Varun Gokulnath 94113a48c2 Add support for LX symlinks (WSL/MSYS2 native symlinks)
- Add reparseTagLxSymlink constant (0xA000001D)
- Implement decode logic for LX symlinks (UTF-8 format)
- Add IsLxSymlink field to ReparsePoint struct to preserve symlink type
- Implement encode logic to recreate LX symlinks on import
- Add unit tests for LX symlink round-trip validation

Fixes issue where Docker builds with MSYS2 failed with 'unsupported reparse point a000001d' error.

Signed-off-by: Varun Gokulnath <vagokuln@microsoft.com>
Signed-off-by: Varun Gokulnath <gvarun22@outlook.com>
2025-12-17 11:16:26 -08:00
5 changed files with 306 additions and 33 deletions
-23
View File
@@ -1,23 +0,0 @@
diff --git a/pipe.go b/pipe.go
index 58c68e9..01cabd7 100644
--- a/pipe.go
+++ b/pipe.go
@@ -123,6 +123,18 @@ type win32MessageBytePipe struct {
type pipeAddress string
+// GetPipeHandle returns the underlying handle of a pipe.
+func GetPipeHandle(f net.Conn) windows.Handle {
+ switch v := f.(type) {
+ case *win32MessageBytePipe:
+ return v.handle
+ case *win32Pipe:
+ return v.handle
+ default:
+ return 0
+ }
+}
+
func (f *win32Pipe) LocalAddr() net.Addr {
return pipeAddress(f.path)
}
+87 -5
View File
@@ -1,7 +1,89 @@
# go-winio - rwinkhart patches # go-winio [![Build Status](https://github.com/microsoft/go-winio/actions/workflows/ci.yml/badge.svg)](https://github.com/microsoft/go-winio/actions/workflows/ci.yml)
This repository holds my custom patches atop the go-winio module.
The only changes in the master branch are this README and the patches in the `1patches` directory. The patches are applied on other branches and tagged releases are available for import from the releases page. This repository contains utilities for efficiently performing Win32 IO operations in
Go. Currently, this is focused on accessing named pipes and other file handles, and
for using named pipes as a net transport.
# How To? This code relies on IO completion ports to avoid blocking IO on system threads, allowing Go
Copy the desired patch file(s) into a new branch (cloned from upstream master) and apply with `patch -p1 < patchfile.patch`. to reuse the thread to schedule another goroutine. This limits support to Windows Vista and
newer operating systems. This is similar to the implementation of network sockets in Go's net
package.
Please see the LICENSE file for licensing information.
## Contributing
This project welcomes contributions and suggestions.
Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that
you have the right to, and actually do, grant us the rights to use your contribution.
For details, visit [Microsoft CLA](https://cla.microsoft.com).
When you submit a pull request, a CLA-bot will automatically determine whether you need to
provide a CLA and decorate the PR appropriately (e.g., label, comment).
Simply follow the instructions provided by the bot.
You will only need to do this once across all repos using our CLA.
Additionally, the pull request pipeline requires the following steps to be performed before
mergining.
### Code Sign-Off
We require that contributors sign their commits using [`git commit --signoff`][git-commit-s]
to certify they either authored the work themselves or otherwise have permission to use it in this project.
A range of commits can be signed off using [`git rebase --signoff`][git-rebase-s].
Please see [the developer certificate](https://developercertificate.org) for more info,
as well as to make sure that you can attest to the rules listed.
Our CI uses the DCO Github app to ensure that all commits in a given PR are signed-off.
### Linting
Code must pass a linting stage, which uses [`golangci-lint`][lint].
The linting settings are stored in [`.golangci.yaml`](./.golangci.yaml), and can be run
automatically with VSCode by adding the following to your workspace or folder settings:
```json
"go.lintTool": "golangci-lint",
"go.lintOnSave": "package",
```
Additional editor [integrations options are also available][lint-ide].
Alternatively, `golangci-lint` can be [installed locally][lint-install] and run from the repo root:
```shell
# use . or specify a path to only lint a package
# to show all lint errors, use flags "--max-issues-per-linter=0 --max-same-issues=0"
> golangci-lint run ./...
```
### Go Generate
The pipeline checks that auto-generated code, via `go generate`, are up to date.
This can be done for the entire repo:
```shell
> go generate ./...
```
## Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
## Special Thanks
Thanks to [natefinch][natefinch] for the inspiration for this library.
See [npipe](https://github.com/natefinch/npipe) for another named pipe implementation.
[lint]: https://golangci-lint.run/
[lint-ide]: https://golangci-lint.run/usage/integrations/#editor-integration
[lint-install]: https://golangci-lint.run/usage/install/#local-installation
[git-commit-s]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt--s
[git-rebase-s]: https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---signoff
[natefinch]: https://github.com/natefinch
+12
View File
@@ -123,6 +123,18 @@ type win32MessageBytePipe struct {
type pipeAddress string type pipeAddress string
// GetPipeHandle returns the underlying handle of a pipe.
func GetPipeHandle(f net.Conn) windows.Handle {
switch v := f.(type) {
case *win32MessageBytePipe:
return v.handle
case *win32Pipe:
return v.handle
default:
return 0
}
}
func (f *win32Pipe) LocalAddr() net.Addr { func (f *win32Pipe) LocalAddr() net.Addr {
return pipeAddress(f.path) return pipeAddress(f.path)
} }
+55 -5
View File
@@ -5,6 +5,7 @@ package winio
import ( import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"strings" "strings"
"unicode/utf16" "unicode/utf16"
@@ -14,6 +15,9 @@ import (
const ( const (
reparseTagMountPoint = 0xA0000003 reparseTagMountPoint = 0xA0000003
reparseTagSymlink = 0xA000000C reparseTagSymlink = 0xA000000C
reparseTagLxSymlink = 0xA000001D // WSL/MSYS2 native symlinks
lxSymlinkVersion = 2 // LX symlink format version
) )
type reparseDataBuffer struct { type reparseDataBuffer struct {
@@ -30,6 +34,7 @@ type reparseDataBuffer struct {
type ReparsePoint struct { type ReparsePoint struct {
Target string Target string
IsMountPoint bool IsMountPoint bool
IsLxSymlink bool // True if this is an LX symlink (WSL/MSYS2 native)
} }
// UnsupportedReparsePointError is returned when trying to decode a non-symlink or // UnsupportedReparsePointError is returned when trying to decode a non-symlink or
@@ -50,14 +55,19 @@ func DecodeReparsePoint(b []byte) (*ReparsePoint, error) {
} }
func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) { func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) {
isMountPoint := false
switch tag { switch tag {
case reparseTagMountPoint: case reparseTagMountPoint:
isMountPoint = true return decodeWindowsReparsePointData(b, true)
case reparseTagSymlink: case reparseTagSymlink:
return decodeWindowsReparsePointData(b, false)
case reparseTagLxSymlink:
return decodeLxReparsePointData(b)
default: default:
return nil, &UnsupportedReparsePointError{tag} return nil, &UnsupportedReparsePointError{tag}
} }
}
func decodeWindowsReparsePointData(b []byte, isMountPoint bool) (*ReparsePoint, error) {
nameOffset := 8 + binary.LittleEndian.Uint16(b[4:6]) nameOffset := 8 + binary.LittleEndian.Uint16(b[4:6])
if !isMountPoint { if !isMountPoint {
nameOffset += 4 nameOffset += 4
@@ -68,16 +78,56 @@ func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &ReparsePoint{string(utf16.Decode(name)), isMountPoint}, nil return &ReparsePoint{Target: string(utf16.Decode(name)), IsMountPoint: isMountPoint, IsLxSymlink: false}, nil
}
func decodeLxReparsePointData(b []byte) (*ReparsePoint, error) {
// LX symlinks store the target as UTF-8 after a 4-byte version field
if len(b) < 4 {
return nil, errors.New("LX symlink buffer too short")
}
targetBytes := b[4:]
for i, c := range targetBytes {
if c == 0 {
targetBytes = targetBytes[:i]
break
}
}
target := string(targetBytes)
return &ReparsePoint{Target: target, IsMountPoint: false, IsLxSymlink: true}, nil
} }
func isDriveLetter(c byte) bool { func isDriveLetter(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
} }
// EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink or // EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink,
// mount point. // mount point, or LX symlink.
func EncodeReparsePoint(rp *ReparsePoint) []byte { func EncodeReparsePoint(rp *ReparsePoint) []byte {
if rp == nil {
return nil
}
if rp.IsLxSymlink {
return encodeLxReparsePoint(rp)
}
return encodeWindowsReparsePoint(rp)
}
func encodeLxReparsePoint(rp *ReparsePoint) []byte {
// LX symlink: 4-byte version + UTF-8 target
targetBytes := []byte(rp.Target)
dataLength := 4 + len(targetBytes)
var b bytes.Buffer
_ = binary.Write(&b, binary.LittleEndian, uint32(reparseTagLxSymlink))
_ = binary.Write(&b, binary.LittleEndian, uint16(dataLength))
_ = binary.Write(&b, binary.LittleEndian, uint16(0))
_ = binary.Write(&b, binary.LittleEndian, uint32(lxSymlinkVersion))
_, _ = b.Write(targetBytes)
return b.Bytes()
}
func encodeWindowsReparsePoint(rp *ReparsePoint) []byte {
// Generate an NT path and determine if this is a relative path. // Generate an NT path and determine if this is a relative path.
var ntTarget string var ntTarget string
relative := false relative := false
+152
View File
@@ -0,0 +1,152 @@
//go:build windows
package winio
import (
"testing"
)
const (
testLxSymlinkAbsolutePath = "/usr/bin/bash"
testWindowsSymlinkPath = `C:\Windows\System32`
testLxSymlinkRelativePath = "../bin/sh"
testLxSymlinkSpecialCharsPath = "/path/with spaces/and-special!@#$%/файл.txt"
)
func TestLxSymlinkRoundTrip(t *testing.T) {
// Test LX symlink encode/decode
original := &ReparsePoint{
Target: testLxSymlinkAbsolutePath,
IsMountPoint: false,
IsLxSymlink: true,
}
// Encode
encoded := EncodeReparsePoint(original)
// Decode
decoded, err := DecodeReparsePoint(encoded)
if err != nil {
t.Fatalf("Failed to decode: %v", err)
}
// Verify
if decoded.Target != original.Target {
t.Errorf("Target mismatch: got %q, want %q", decoded.Target, original.Target)
}
if decoded.IsLxSymlink != original.IsLxSymlink {
t.Errorf("IsLxSymlink mismatch: got %v, want %v", decoded.IsLxSymlink, original.IsLxSymlink)
}
if decoded.IsMountPoint != original.IsMountPoint {
t.Errorf("IsMountPoint mismatch: got %v, want %v", decoded.IsMountPoint, original.IsMountPoint)
}
}
func TestWindowsSymlinkNotLx(t *testing.T) {
// Test that regular Windows symlinks are not marked as LX
original := &ReparsePoint{
Target: testWindowsSymlinkPath,
IsMountPoint: false,
IsLxSymlink: false,
}
// Encode
encoded := EncodeReparsePoint(original)
// Decode
decoded, err := DecodeReparsePoint(encoded)
if err != nil {
t.Fatalf("Failed to decode: %v", err)
}
// Verify it's NOT an LX symlink
if decoded.IsLxSymlink {
t.Errorf("Windows symlink incorrectly marked as LX symlink")
}
}
func TestLxSymlinkEmptyTarget(t *testing.T) {
// Test LX symlink with empty target
original := &ReparsePoint{
Target: "",
IsMountPoint: false,
IsLxSymlink: true,
}
// Encode
encoded := EncodeReparsePoint(original)
// Decode
decoded, err := DecodeReparsePoint(encoded)
if err != nil {
t.Fatalf("Failed to decode: %v", err)
}
// Verify
if decoded.Target != original.Target {
t.Errorf("Target mismatch: got %q, want %q", decoded.Target, original.Target)
}
if !decoded.IsLxSymlink {
t.Errorf("IsLxSymlink should be true")
}
}
func TestLxSymlinkRelativePath(t *testing.T) {
// Test LX symlink with relative path
original := &ReparsePoint{
Target: testLxSymlinkRelativePath,
IsMountPoint: false,
IsLxSymlink: true,
}
// Encode
encoded := EncodeReparsePoint(original)
// Decode
decoded, err := DecodeReparsePoint(encoded)
if err != nil {
t.Fatalf("Failed to decode: %v", err)
}
// Verify
if decoded.Target != original.Target {
t.Errorf("Target mismatch: got %q, want %q", decoded.Target, original.Target)
}
if !decoded.IsLxSymlink {
t.Errorf("IsLxSymlink should be true")
}
}
func TestLxSymlinkSpecialCharacters(t *testing.T) {
// Test LX symlink with special characters and Unicode
original := &ReparsePoint{
Target: testLxSymlinkSpecialCharsPath,
IsMountPoint: false,
IsLxSymlink: true,
}
// Encode
encoded := EncodeReparsePoint(original)
// Decode
decoded, err := DecodeReparsePoint(encoded)
if err != nil {
t.Fatalf("Failed to decode: %v", err)
}
// Verify
if decoded.Target != original.Target {
t.Errorf("Target mismatch: got %q, want %q", decoded.Target, original.Target)
}
if !decoded.IsLxSymlink {
t.Errorf("IsLxSymlink should be true")
}
}
func TestEncodeReparsePointNil(t *testing.T) {
// Test encoding a nil ReparsePoint
encoded := EncodeReparsePoint(nil)
if encoded != nil {
t.Errorf("Expected nil result for nil input, got %v", encoded)
}
}