From 9f9cf28f8fe67e6c17123cae2d89f116504f2be1 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Tue, 13 May 2025 15:35:19 -0700 Subject: [PATCH] [release-branch.go1.24] os: don't follow symlinks on Windows when O_CREATE|O_EXCL (This cherry-pick includes both CL 672396 and CL 676655.) Match standard Unix behavior: Symlinks are not followed when O_CREATE|O_EXCL is passed to open. Thanks to Junyoung Park and Dong-uk Kim of KAIST Hacking Lab for discovering this issue. For #73702 Fixed #73720 Fixes CVE-2025-0913 Change-Id: Ieb46a6780c5e9a6090b09cd34290f04a8e3b0ca5 Reviewed-on: https://go-review.googlesource.com/c/go/+/672396 Auto-Submit: Damien Neil LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan Reviewed-on: https://go-review.googlesource.com/c/go/+/677215 Reviewed-by: Michael Knyszek TryBot-Bypass: Michael Knyszek --- src/internal/syscall/windows/at_windows.go | 1 + src/os/os_test.go | 25 +++++++++++++++++++ src/syscall/syscall_windows.go | 29 +++++++++++----------- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/internal/syscall/windows/at_windows.go b/src/internal/syscall/windows/at_windows.go index 18429773c0..05170b218e 100644 --- a/src/internal/syscall/windows/at_windows.go +++ b/src/internal/syscall/windows/at_windows.go @@ -88,6 +88,7 @@ func Openat(dirfd syscall.Handle, name string, flag int, perm uint32) (_ syscall switch { case flag&(syscall.O_CREAT|syscall.O_EXCL) == (syscall.O_CREAT | syscall.O_EXCL): disposition = FILE_CREATE + options |= FILE_OPEN_REPARSE_POINT // don't follow symlinks case flag&syscall.O_CREAT == syscall.O_CREAT: disposition = FILE_OPEN_IF default: diff --git a/src/os/os_test.go b/src/os/os_test.go index 4ddbe6022b..3b348f93d2 100644 --- a/src/os/os_test.go +++ b/src/os/os_test.go @@ -2223,6 +2223,31 @@ func TestFilePermissions(t *testing.T) { } +func TestOpenFileCreateExclDanglingSymlink(t *testing.T) { + testMaybeRooted(t, func(t *testing.T, r *Root) { + const link = "link" + if err := Symlink("does_not_exist", link); err != nil { + t.Fatal(err) + } + var f *File + var err error + if r == nil { + f, err = OpenFile(link, O_WRONLY|O_CREATE|O_EXCL, 0o444) + } else { + f, err = r.OpenFile(link, O_WRONLY|O_CREATE|O_EXCL, 0o444) + } + if err == nil { + f.Close() + } + if !errors.Is(err, ErrExist) { + t.Errorf("OpenFile of a dangling symlink with O_CREATE|O_EXCL = %v, want ErrExist", err) + } + if _, err := Stat(link); err == nil { + t.Errorf("OpenFile of a dangling symlink with O_CREATE|O_EXCL created a file") + } + }) +} + // TestFileRDWRFlags tests the O_RDONLY, O_WRONLY, and O_RDWR flags. func TestFileRDWRFlags(t *testing.T) { for _, test := range []struct { diff --git a/src/syscall/syscall_windows.go b/src/syscall/syscall_windows.go index 344f6c325c..1c5a67778f 100644 --- a/src/syscall/syscall_windows.go +++ b/src/syscall/syscall_windows.go @@ -376,20 +376,6 @@ func Open(name string, flag int, perm uint32) (fd Handle, err error) { if flag&O_CLOEXEC == 0 { sa = makeInheritSa() } - // We don't use CREATE_ALWAYS, because when opening a file with - // FILE_ATTRIBUTE_READONLY these will replace an existing file - // with a new, read-only one. See https://go.dev/issue/38225. - // - // Instead, we ftruncate the file after opening when O_TRUNC is set. - var createmode uint32 - switch { - case flag&(O_CREAT|O_EXCL) == (O_CREAT | O_EXCL): - createmode = CREATE_NEW - case flag&O_CREAT == O_CREAT: - createmode = OPEN_ALWAYS - default: - createmode = OPEN_EXISTING - } var attrs uint32 = FILE_ATTRIBUTE_NORMAL if perm&S_IWRITE == 0 { attrs = FILE_ATTRIBUTE_READONLY @@ -404,6 +390,21 @@ func Open(name string, flag int, perm uint32) (fd Handle, err error) { const _FILE_FLAG_WRITE_THROUGH = 0x80000000 attrs |= _FILE_FLAG_WRITE_THROUGH } + // We don't use CREATE_ALWAYS, because when opening a file with + // FILE_ATTRIBUTE_READONLY these will replace an existing file + // with a new, read-only one. See https://go.dev/issue/38225. + // + // Instead, we ftruncate the file after opening when O_TRUNC is set. + var createmode uint32 + switch { + case flag&(O_CREAT|O_EXCL) == (O_CREAT | O_EXCL): + createmode = CREATE_NEW + attrs |= FILE_FLAG_OPEN_REPARSE_POINT // don't follow symlinks + case flag&O_CREAT == O_CREAT: + createmode = OPEN_ALWAYS + default: + createmode = OPEN_EXISTING + } h, err := createFile(namep, access, sharemode, sa, createmode, attrs, 0) if h == InvalidHandle { if err == ERROR_ACCESS_DENIED && (flag&O_WRONLY != 0 || flag&O_RDWR != 0) { -- 2.50.0