diff options
| author | Damien Neil <dneil@google.com> | 2025-03-25 10:31:00 -0700 |
|---|---|---|
| committer | Gopher Robot <gobot@golang.org> | 2025-03-28 11:02:40 -0700 |
| commit | 26fdb07d4ce58885305283ba18960f582f4eaa73 (patch) | |
| tree | 6291830ac80f793d1979ce338377f4556c86247a /src/internal | |
| parent | 656b5b3abe25d026725edff49edbdaa9862c9d77 (diff) | |
| download | go-26fdb07d4ce58885305283ba18960f582f4eaa73.tar.xz | |
os: add Root.Symlink
For #67002
Change-Id: Ia1637b61eae49e97e1d07f058ad2390e74cd3403
Reviewed-on: https://go-review.googlesource.com/c/go/+/660635
Reviewed-by: Alan Donovan <adonovan@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Quim Muntal <quimmuntal@gmail.com>
Auto-Submit: Damien Neil <dneil@google.com>
Diffstat (limited to 'src/internal')
| -rw-r--r-- | src/internal/syscall/unix/asm_darwin.s | 1 | ||||
| -rw-r--r-- | src/internal/syscall/unix/asm_openbsd.s | 2 | ||||
| -rw-r--r-- | src/internal/syscall/unix/at.go | 19 | ||||
| -rw-r--r-- | src/internal/syscall/unix/at_aix.go | 1 | ||||
| -rw-r--r-- | src/internal/syscall/unix/at_darwin.go | 26 | ||||
| -rw-r--r-- | src/internal/syscall/unix/at_libc.go | 24 | ||||
| -rw-r--r-- | src/internal/syscall/unix/at_openbsd.go | 26 | ||||
| -rw-r--r-- | src/internal/syscall/unix/at_solaris.go | 1 | ||||
| -rw-r--r-- | src/internal/syscall/unix/at_sysnum_dragonfly.go | 1 | ||||
| -rw-r--r-- | src/internal/syscall/unix/at_sysnum_freebsd.go | 1 | ||||
| -rw-r--r-- | src/internal/syscall/unix/at_sysnum_linux.go | 1 | ||||
| -rw-r--r-- | src/internal/syscall/unix/at_sysnum_netbsd.go | 1 | ||||
| -rw-r--r-- | src/internal/syscall/unix/at_wasip1.go | 33 | ||||
| -rw-r--r-- | src/internal/syscall/windows/at_windows.go | 175 |
14 files changed, 303 insertions, 9 deletions
diff --git a/src/internal/syscall/unix/asm_darwin.s b/src/internal/syscall/unix/asm_darwin.s index 79d384c941..9803c7260f 100644 --- a/src/internal/syscall/unix/asm_darwin.s +++ b/src/internal/syscall/unix/asm_darwin.s @@ -29,3 +29,4 @@ TEXT ·libc_fchmodat_trampoline(SB),NOSPLIT,$0-0; JMP libc_fchmodat(SB) TEXT ·libc_fchownat_trampoline(SB),NOSPLIT,$0-0; JMP libc_fchownat(SB) TEXT ·libc_renameat_trampoline(SB),NOSPLIT,$0-0; JMP libc_renameat(SB) TEXT ·libc_linkat_trampoline(SB),NOSPLIT,$0-0; JMP libc_linkat(SB) +TEXT ·libc_symlinkat_trampoline(SB),NOSPLIT,$0-0; JMP libc_symlinkat(SB) diff --git a/src/internal/syscall/unix/asm_openbsd.s b/src/internal/syscall/unix/asm_openbsd.s index 481dd7d700..d7c230555c 100644 --- a/src/internal/syscall/unix/asm_openbsd.s +++ b/src/internal/syscall/unix/asm_openbsd.s @@ -22,3 +22,5 @@ TEXT ·libc_renameat_trampoline(SB),NOSPLIT,$0-0 JMP libc_renameat(SB) TEXT ·libc_linkat_trampoline(SB),NOSPLIT,$0-0 JMP libc_linkat(SB) +TEXT ·libc_symlinkat_trampoline(SB),NOSPLIT,$0-0 + JMP libc_symlinkat(SB) diff --git a/src/internal/syscall/unix/at.go b/src/internal/syscall/unix/at.go index 4549a07f8c..96272afc7b 100644 --- a/src/internal/syscall/unix/at.go +++ b/src/internal/syscall/unix/at.go @@ -158,3 +158,22 @@ func Linkat(olddirfd int, oldpath string, newdirfd int, newpath string, flag int } return nil } + +func Symlinkat(oldpath string, newdirfd int, newpath string) error { + oldp, err := syscall.BytePtrFromString(oldpath) + if err != nil { + return err + } + newp, err := syscall.BytePtrFromString(newpath) + if err != nil { + return err + } + _, _, errno := syscall.Syscall(symlinkatTrap, + uintptr(unsafe.Pointer(oldp)), + uintptr(newdirfd), + uintptr(unsafe.Pointer(newp))) + if errno != 0 { + return errno + } + return nil +} diff --git a/src/internal/syscall/unix/at_aix.go b/src/internal/syscall/unix/at_aix.go index 573554927e..8bf7b4dd81 100644 --- a/src/internal/syscall/unix/at_aix.go +++ b/src/internal/syscall/unix/at_aix.go @@ -10,6 +10,7 @@ package unix //go:cgo_import_dynamic libc_linkat linkat "libc.a/shr_64.o" //go:cgo_import_dynamic libc_openat openat "libc.a/shr_64.o" //go:cgo_import_dynamic libc_renameat renameat "libc.a/shr_64.o" +//go:cgo_import_dynamic libc_symlinkat symlinkat "libc.a/shr_64.o" //go:cgo_import_dynamic libc_unlinkat unlinkat "libc.a/shr_64.o" //go:cgo_import_dynamic libc_readlinkat readlinkat "libc.a/shr_64.o" //go:cgo_import_dynamic libc_mkdirat mkdirat "libc.a/shr_64.o" diff --git a/src/internal/syscall/unix/at_darwin.go b/src/internal/syscall/unix/at_darwin.go index 61437672ee..c74c827626 100644 --- a/src/internal/syscall/unix/at_darwin.go +++ b/src/internal/syscall/unix/at_darwin.go @@ -154,3 +154,29 @@ func Linkat(olddirfd int, oldpath string, newdirfd int, newpath string, flag int } return nil } + +func libc_symlinkat_trampoline() + +//go:cgo_import_dynamic libc_symlinkat symlinkat "/usr/lib/libSystem.B.dylib" + +func Symlinkat(oldpath string, newdirfd int, newpath string) error { + oldp, err := syscall.BytePtrFromString(oldpath) + if err != nil { + return err + } + newp, err := syscall.BytePtrFromString(newpath) + if err != nil { + return err + } + _, _, errno := syscall_syscall6(abi.FuncPCABI0(libc_symlinkat_trampoline), + uintptr(unsafe.Pointer(oldp)), + uintptr(newdirfd), + uintptr(unsafe.Pointer(newp)), + 0, + 0, + 0) + if errno != 0 { + return errno + } + return nil +} diff --git a/src/internal/syscall/unix/at_libc.go b/src/internal/syscall/unix/at_libc.go index b32d3bba39..5c64b34d48 100644 --- a/src/internal/syscall/unix/at_libc.go +++ b/src/internal/syscall/unix/at_libc.go @@ -20,6 +20,7 @@ import ( //go:linkname procFchownat libc_fchownat //go:linkname procRenameat libc_renameat //go:linkname procLinkat libc_linkat +//go:linkname procSymlinkat libc_symlinkat var ( procFstatat, @@ -30,7 +31,8 @@ var ( procFchmodat, procFchownat, procRenameat, - procLinkat uintptr + procLinkat, + procSymlinkat uintptr ) func Unlinkat(dirfd int, path string, flags int) error { @@ -208,3 +210,23 @@ func Linkat(olddirfd int, oldpath string, newdirfd int, newpath string, flag int } return nil } + +func Symlinkat(oldpath string, newdirfd int, newpath string) error { + oldp, err := syscall.BytePtrFromString(oldpath) + if err != nil { + return err + } + newp, err := syscall.BytePtrFromString(newpath) + if err != nil { + return err + } + _, _, errno := syscall6(uintptr(unsafe.Pointer(&procSymlinkat)), 3, + uintptr(unsafe.Pointer(oldp)), + uintptr(newdirfd), + uintptr(unsafe.Pointer(newp)), + 0, 0, 0) + if errno != 0 { + return errno + } + return nil +} diff --git a/src/internal/syscall/unix/at_openbsd.go b/src/internal/syscall/unix/at_openbsd.go index 2a433930f3..0fd5e90e5c 100644 --- a/src/internal/syscall/unix/at_openbsd.go +++ b/src/internal/syscall/unix/at_openbsd.go @@ -145,3 +145,29 @@ func Linkat(olddirfd int, oldpath string, newdirfd int, newpath string, flag int } return nil } + +func libc_symlinkat_trampoline() + +//go:cgo_import_dynamic libc_symlinkat symlinkat "libc.so" + +func Symlinkat(oldpath string, newdirfd int, newpath string) error { + oldp, err := syscall.BytePtrFromString(oldpath) + if err != nil { + return err + } + newp, err := syscall.BytePtrFromString(newpath) + if err != nil { + return err + } + _, _, errno := syscall_syscall6(abi.FuncPCABI0(libc_symlinkat_trampoline), + uintptr(unsafe.Pointer(oldp)), + uintptr(newdirfd), + uintptr(unsafe.Pointer(newp)), + 0, + 0, + 0) + if errno != 0 { + return errno + } + return nil +} diff --git a/src/internal/syscall/unix/at_solaris.go b/src/internal/syscall/unix/at_solaris.go index abfda15688..5d69ae5bee 100644 --- a/src/internal/syscall/unix/at_solaris.go +++ b/src/internal/syscall/unix/at_solaris.go @@ -19,6 +19,7 @@ func rawSyscall6(trap, nargs, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, e //go:cgo_import_dynamic libc_linkat linkat "libc.so" //go:cgo_import_dynamic libc_openat openat "libc.so" //go:cgo_import_dynamic libc_renameat renameat "libc.so" +//go:cgo_import_dynamic libc_symlinkat symlinkat "libc.so" //go:cgo_import_dynamic libc_unlinkat unlinkat "libc.so" //go:cgo_import_dynamic libc_readlinkat readlinkat "libc.so" //go:cgo_import_dynamic libc_mkdirat mkdirat "libc.so" diff --git a/src/internal/syscall/unix/at_sysnum_dragonfly.go b/src/internal/syscall/unix/at_sysnum_dragonfly.go index 3ba2c54152..9728b969c4 100644 --- a/src/internal/syscall/unix/at_sysnum_dragonfly.go +++ b/src/internal/syscall/unix/at_sysnum_dragonfly.go @@ -16,6 +16,7 @@ const ( fchownatTrap uintptr = syscall.SYS_FCHOWNAT renameatTrap uintptr = syscall.SYS_RENAMEAT linkatTrap uintptr = syscall.SYS_LINKAT + symlinkatTrap uintptr = syscall.SYS_SYMLINKAT AT_EACCESS = 0x4 AT_FDCWD = 0xfffafdcd diff --git a/src/internal/syscall/unix/at_sysnum_freebsd.go b/src/internal/syscall/unix/at_sysnum_freebsd.go index 032b8b5276..c1fdcabf41 100644 --- a/src/internal/syscall/unix/at_sysnum_freebsd.go +++ b/src/internal/syscall/unix/at_sysnum_freebsd.go @@ -23,4 +23,5 @@ const ( fchownatTrap uintptr = syscall.SYS_FCHOWNAT renameatTrap uintptr = syscall.SYS_RENAMEAT linkatTrap uintptr = syscall.SYS_LINKAT + symlinkatTrap uintptr = syscall.SYS_SYMLINKAT ) diff --git a/src/internal/syscall/unix/at_sysnum_linux.go b/src/internal/syscall/unix/at_sysnum_linux.go index 6b8bebff2a..bb7f244fe2 100644 --- a/src/internal/syscall/unix/at_sysnum_linux.go +++ b/src/internal/syscall/unix/at_sysnum_linux.go @@ -14,6 +14,7 @@ const ( fchmodatTrap uintptr = syscall.SYS_FCHMODAT fchownatTrap uintptr = syscall.SYS_FCHOWNAT linkatTrap uintptr = syscall.SYS_LINKAT + symlinkatTrap uintptr = syscall.SYS_SYMLINKAT ) const ( diff --git a/src/internal/syscall/unix/at_sysnum_netbsd.go b/src/internal/syscall/unix/at_sysnum_netbsd.go index 01e10ddd59..b59b5e0cf9 100644 --- a/src/internal/syscall/unix/at_sysnum_netbsd.go +++ b/src/internal/syscall/unix/at_sysnum_netbsd.go @@ -16,6 +16,7 @@ const ( fchownatTrap uintptr = syscall.SYS_FCHOWNAT renameatTrap uintptr = syscall.SYS_RENAMEAT linkatTrap uintptr = syscall.SYS_LINKAT + symlinkatTrap uintptr = syscall.SYS_SYMLINKAT ) const ( diff --git a/src/internal/syscall/unix/at_wasip1.go b/src/internal/syscall/unix/at_wasip1.go index 72537caf1e..dfbb365f2a 100644 --- a/src/internal/syscall/unix/at_wasip1.go +++ b/src/internal/syscall/unix/at_wasip1.go @@ -101,6 +101,10 @@ func Mkdirat(dirfd int, path string, mode uint32) error { )) } +//go:wasmimport wasi_snapshot_preview1 path_create_directory +//go:noescape +func path_create_directory(fd int32, path *byte, pathLen size) syscall.Errno + func Fchmodat(dirfd int, path string, mode uint32, flags int) error { // WASI preview 1 doesn't support changing file modes. return syscall.ENOSYS @@ -111,10 +115,6 @@ func Fchownat(dirfd int, path string, uid, gid int, flags int) error { return syscall.ENOSYS } -//go:wasmimport wasi_snapshot_preview1 path_rename -//go:noescape -func path_rename(oldFd int32, oldPath *byte, oldPathLen size, newFd int32, newPath *byte, newPathLen size) syscall.Errno - func Renameat(olddirfd int, oldpath string, newdirfd int, newpath string) error { if oldpath == "" || newpath == "" { return syscall.EINVAL @@ -129,9 +129,9 @@ func Renameat(olddirfd int, oldpath string, newdirfd int, newpath string) error )) } -//go:wasmimport wasi_snapshot_preview1 path_link +//go:wasmimport wasi_snapshot_preview1 path_rename //go:noescape -func path_link(oldFd int32, oldFlags uint32, oldPath *byte, oldPathLen size, newFd int32, newPath *byte, newPathLen size) syscall.Errno +func path_rename(oldFd int32, oldPath *byte, oldPathLen size, newFd int32, newPath *byte, newPathLen size) syscall.Errno func Linkat(olddirfd int, oldpath string, newdirfd int, newpath string, flag int) error { if oldpath == "" || newpath == "" { @@ -148,9 +148,26 @@ func Linkat(olddirfd int, oldpath string, newdirfd int, newpath string, flag int )) } -//go:wasmimport wasi_snapshot_preview1 path_create_directory +//go:wasmimport wasi_snapshot_preview1 path_link //go:noescape -func path_create_directory(fd int32, path *byte, pathLen size) syscall.Errno +func path_link(oldFd int32, oldFlags uint32, oldPath *byte, oldPathLen size, newFd int32, newPath *byte, newPathLen size) syscall.Errno + +func Symlinkat(oldpath string, newdirfd int, newpath string) error { + if oldpath == "" || newpath == "" { + return syscall.EINVAL + } + return errnoErr(path_symlink( + unsafe.StringData(oldpath), + size(len(oldpath)), + int32(newdirfd), + unsafe.StringData(newpath), + size(len(newpath)), + )) +} + +//go:wasmimport wasi_snapshot_preview1 path_symlink +//go:noescape +func path_symlink(oldPath *byte, oldPathLen size, fd int32, newPath *byte, newPathLen size) syscall.Errno func errnoErr(errno syscall.Errno) error { if errno == 0 { diff --git a/src/internal/syscall/windows/at_windows.go b/src/internal/syscall/windows/at_windows.go index 4b939d46ab..f04de276b9 100644 --- a/src/internal/syscall/windows/at_windows.go +++ b/src/internal/syscall/windows/at_windows.go @@ -5,6 +5,8 @@ package windows import ( + "runtime" + "structs" "syscall" "unsafe" ) @@ -376,3 +378,176 @@ func Linkat(olddirfd syscall.Handle, oldpath string, newdirfd syscall.Handle, ne } return err } + +// SymlinkatFlags configure Symlinkat. +// +// Symbolic links have two properties: They may be directory or file links, +// and they may be absolute or relative. +// +// The Windows API defines flags describing these properties +// (SYMBOLIC_LINK_FLAG_DIRECTORY and SYMLINK_FLAG_RELATIVE), +// but the flags are passed to different system calls and +// do not have distinct values, so we define our own enumeration +// that permits expressing both. +type SymlinkatFlags uint + +const ( + SYMLINKAT_DIRECTORY = SymlinkatFlags(1 << iota) + SYMLINKAT_RELATIVE +) + +func Symlinkat(oldname string, newdirfd syscall.Handle, newname string, flags SymlinkatFlags) error { + // Temporarily acquire symlink-creating privileges if possible. + // This is the behavior of CreateSymbolicLinkW. + // + // (When passed the SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE flag, + // CreateSymbolicLinkW ignores errors in acquiring privileges, as we do here.) + return withPrivilege("SeCreateSymbolicLinkPrivilege", func() error { + return symlinkat(oldname, newdirfd, newname, flags) + }) +} + +func symlinkat(oldname string, newdirfd syscall.Handle, newname string, flags SymlinkatFlags) error { + oldnameu16, err := syscall.UTF16FromString(oldname) + if err != nil { + return err + } + oldnameu16 = oldnameu16[:len(oldnameu16)-1] // trim off terminal NUL + + var options uint32 + if flags&SYMLINKAT_DIRECTORY != 0 { + options |= FILE_DIRECTORY_FILE + } else { + options |= FILE_NON_DIRECTORY_FILE + } + + objAttrs := &OBJECT_ATTRIBUTES{} + if err := objAttrs.init(newdirfd, newname); err != nil { + return err + } + var h syscall.Handle + err = NtCreateFile( + &h, + SYNCHRONIZE|FILE_WRITE_ATTRIBUTES|DELETE, + objAttrs, + &IO_STATUS_BLOCK{}, + nil, + syscall.FILE_ATTRIBUTE_NORMAL, + 0, + FILE_CREATE, + FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|FILE_SYNCHRONOUS_IO_NONALERT|options, + 0, + 0, + ) + if err != nil { + return ntCreateFileError(err, 0) + } + defer syscall.CloseHandle(h) + + // https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_reparse_data_buffer + type reparseDataBufferT struct { + _ structs.HostLayout + + ReparseTag uint32 + ReparseDataLength uint16 + Reserved uint16 + + SubstituteNameOffset uint16 + SubstituteNameLength uint16 + PrintNameOffset uint16 + PrintNameLength uint16 + Flags uint32 + } + + const ( + headerSize = uint16(unsafe.Offsetof(reparseDataBufferT{}.SubstituteNameOffset)) + bufferSize = uint16(unsafe.Sizeof(reparseDataBufferT{})) + ) + + // Data buffer containing a SymbolicLinkReparseBuffer followed by the link target. + rdbbuf := make([]byte, bufferSize+uint16(2*len(oldnameu16))) + + rdb := (*reparseDataBufferT)(unsafe.Pointer(&rdbbuf[0])) + rdb.ReparseTag = syscall.IO_REPARSE_TAG_SYMLINK + rdb.ReparseDataLength = uint16(len(rdbbuf)) - uint16(headerSize) + rdb.SubstituteNameOffset = 0 + rdb.SubstituteNameLength = uint16(2 * len(oldnameu16)) + rdb.PrintNameOffset = 0 + rdb.PrintNameLength = rdb.SubstituteNameLength + if flags&SYMLINKAT_RELATIVE != 0 { + rdb.Flags = SYMLINK_FLAG_RELATIVE + } + + namebuf := rdbbuf[bufferSize:] + copy(namebuf, unsafe.String((*byte)(unsafe.Pointer(&oldnameu16[0])), 2*len(oldnameu16))) + + err = syscall.DeviceIoControl( + h, + FSCTL_SET_REPARSE_POINT, + &rdbbuf[0], + uint32(len(rdbbuf)), + nil, + 0, + nil, + nil) + if err != nil { + // Creating the symlink has failed, so try to remove the file. + const FileDispositionInformation = 13 + NtSetInformationFile( + h, + &IO_STATUS_BLOCK{}, + uintptr(unsafe.Pointer(&FILE_DISPOSITION_INFORMATION{ + DeleteFile: true, + })), + uint32(unsafe.Sizeof(FILE_DISPOSITION_INFORMATION{})), + FileDispositionInformation, + ) + return err + } + + return nil +} + +// withPrivilege temporariliy acquires the named privilege and runs f. +// If the privilege cannot be acquired it runs f anyway, +// which should fail with an appropriate error. +func withPrivilege(privilege string, f func() error) error { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + err := ImpersonateSelf(SecurityImpersonation) + if err != nil { + return f() + } + defer RevertToSelf() + + curThread, err := GetCurrentThread() + if err != nil { + return f() + } + var token syscall.Token + err = OpenThreadToken(curThread, syscall.TOKEN_QUERY|TOKEN_ADJUST_PRIVILEGES, false, &token) + if err != nil { + return f() + } + defer syscall.CloseHandle(syscall.Handle(token)) + + privStr, err := syscall.UTF16PtrFromString(privilege) + if err != nil { + return f() + } + var tokenPriv TOKEN_PRIVILEGES + err = LookupPrivilegeValue(nil, privStr, &tokenPriv.Privileges[0].Luid) + if err != nil { + return f() + } + + tokenPriv.PrivilegeCount = 1 + tokenPriv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED + err = AdjustTokenPrivileges(token, false, &tokenPriv, 0, nil, nil) + if err != nil { + return f() + } + + return f() +} |
