From be720800896e2e50c9a84794803151650022fede Mon Sep 17 00:00:00 2001 From: mohammadmseet-hue Date: Tue, 7 Apr 2026 04:06:49 +0200 Subject: [PATCH] windows: fix OOB read in Readlink reparse point parsing Readlink on Windows uses subtraction instead of addition when computing the PrintName slice from a mount point or symlink reparse data buffer. Per the Windows REPARSE_DATA_BUFFER documentation, PrintNameOffset is the byte offset and PrintNameLength is the byte length of the print name. The correct end index is (PrintNameOffset + PrintNameLength) / 2, but the code computes (PrintNameLength - PrintNameOffset) / 2. When PrintNameOffset is non-zero (common for symlinks where the substitute name precedes the print name in PathBuffer), the subtraction produces a smaller-than-intended or negative slice index, causing either a truncated result or an out-of-bounds heap read of up to ~64KB. The fix changes the two subtraction operations to addition on both the IO_REPARSE_TAG_SYMLINK and IO_REPARSE_TAG_MOUNT_POINT code paths. Tests are added covering file symlinks, directory symlinks, and directory junctions. Change-Id: Iece12b37ea0655b6b397135a6b3959147e1dc16f --- windows/syscall_windows.go | 4 +-- windows/syscall_windows_test.go | 61 +++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/windows/syscall_windows.go b/windows/syscall_windows.go index d76643658..0f20c3595 100644 --- a/windows/syscall_windows.go +++ b/windows/syscall_windows.go @@ -1516,11 +1516,11 @@ func Readlink(path string, buf []byte) (n int, err error) { case IO_REPARSE_TAG_SYMLINK: data := (*symbolicLinkReparseBuffer)(unsafe.Pointer(&rdb.reparseBuffer)) p := (*[0xffff]uint16)(unsafe.Pointer(&data.PathBuffer[0])) - s = UTF16ToString(p[data.PrintNameOffset/2 : (data.PrintNameLength-data.PrintNameOffset)/2]) + s = UTF16ToString(p[data.PrintNameOffset/2 : (data.PrintNameOffset+data.PrintNameLength)/2]) case IO_REPARSE_TAG_MOUNT_POINT: data := (*mountPointReparseBuffer)(unsafe.Pointer(&rdb.reparseBuffer)) p := (*[0xffff]uint16)(unsafe.Pointer(&data.PathBuffer[0])) - s = UTF16ToString(p[data.PrintNameOffset/2 : (data.PrintNameLength-data.PrintNameOffset)/2]) + s = UTF16ToString(p[data.PrintNameOffset/2 : (data.PrintNameOffset+data.PrintNameLength)/2]) default: // the path is not a symlink or junction but another type of reparse // point diff --git a/windows/syscall_windows_test.go b/windows/syscall_windows_test.go index 761153844..3d8790053 100644 --- a/windows/syscall_windows_test.go +++ b/windows/syscall_windows_test.go @@ -1512,3 +1512,64 @@ func TestIsProcessorFeaturePresent(t *testing.T) { t.Fatal("IsProcessorFeaturePresent failed, but should succeed") } } + +func TestReadlink(t *testing.T) { + tmpdir := t.TempDir() + + // Test 1: Symlink to a file. + // When Windows creates a symlink, SubstituteName typically precedes + // PrintName in PathBuffer, making PrintNameOffset > 0. + target := filepath.Join(tmpdir, "target") + if err := os.WriteFile(target, []byte("hello"), 0666); err != nil { + t.Fatal(err) + } + symlink := filepath.Join(tmpdir, "symlink") + if err := os.Symlink(target, symlink); err != nil { + t.Fatalf("os.Symlink failed: %v", err) + } + buf := make([]byte, windows.MAXIMUM_REPARSE_DATA_BUFFER_SIZE) + n, err := windows.Readlink(symlink, buf) + if err != nil { + t.Fatalf("Readlink(%q) failed: %v", symlink, err) + } + got := string(buf[:n]) + if got != target { + t.Errorf("Readlink(%q) = %q, want %q", symlink, got, target) + } + + // Test 2: Symlink to a directory. + targetDir := filepath.Join(tmpdir, "targetdir") + if err := os.Mkdir(targetDir, 0777); err != nil { + t.Fatal(err) + } + symlinkDir := filepath.Join(tmpdir, "symlinkdir") + if err := os.Symlink(targetDir, symlinkDir); err != nil { + t.Fatalf("os.Symlink failed: %v", err) + } + n, err = windows.Readlink(symlinkDir, buf) + if err != nil { + t.Fatalf("Readlink(%q) failed: %v", symlinkDir, err) + } + got = string(buf[:n]) + if got != targetDir { + t.Errorf("Readlink(%q) = %q, want %q", symlinkDir, got, targetDir) + } + + // Test 3: Directory junction (IO_REPARSE_TAG_MOUNT_POINT). + // Junctions also store SubstituteName before PrintName in PathBuffer, + // so PrintNameOffset > 0, exercising the same code path. + junction := filepath.Join(tmpdir, "junction") + cmd := exec.Command("cmd", "/c", "mklink", "/J", junction, targetDir) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("mklink /J failed: %v - %s", err, out) + } + n, err = windows.Readlink(junction, buf) + if err != nil { + t.Fatalf("Readlink(%q) failed: %v", junction, err) + } + got = string(buf[:n]) + if got != targetDir { + t.Errorf("Readlink(%q) = %q, want %q", junction, got, targetDir) + } +}