os: add Root.Link

For #67002

Change-Id: I223f3f2dbc8b02726f4ce5a017c628c4a20f109a
Reviewed-on: https://go-review.googlesource.com/c/go/+/659757
Reviewed-by: Quim Muntal <quimmuntal@gmail.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Damien Neil <dneil@google.com>
Reviewed-by: Ian Lance Taylor <iant@google.com>
This commit is contained in:
Damien Neil 2025-03-20 12:41:21 -07:00 committed by Gopher Robot
parent 4ae6ab2bdf
commit d2d1fd68b6
24 changed files with 379 additions and 23 deletions

View File

@ -2,5 +2,6 @@ pkg os, method (*Root) Chmod(string, fs.FileMode) error #67002
pkg os, method (*Root) Chown(string, int, int) error #67002
pkg os, method (*Root) Chtimes(string, time.Time, time.Time) error #67002
pkg os, method (*Root) Lchown(string, int, int) error #67002
pkg os, method (*Root) Link(string, string) error #67002
pkg os, method (*Root) Readlink(string) (string, error) #67002
pkg os, method (*Root) Rename(string, string) error #67002

View File

@ -4,5 +4,6 @@ The [os.Root] type supports the following additional methods:
* [os.Root.Chown]
* [os.Root.Chtimes]
* [os.Root.Lchown]
* [os.Root.Link]
* [os.Root.Readlink]
* [os.Root.Rename]

View File

@ -28,3 +28,4 @@ TEXT ·libc_mkdirat_trampoline(SB),NOSPLIT,$0-0; JMP libc_mkdirat(SB)
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)

View File

@ -20,3 +20,5 @@ 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)

View File

@ -136,3 +136,25 @@ func Renameat(olddirfd int, oldpath string, newdirfd int, newpath string) error
}
return nil
}
func Linkat(olddirfd int, oldpath string, newdirfd int, newpath string, flag int) error {
oldp, err := syscall.BytePtrFromString(oldpath)
if err != nil {
return err
}
newp, err := syscall.BytePtrFromString(newpath)
if err != nil {
return err
}
_, _, errno := syscall.Syscall6(linkatTrap,
uintptr(olddirfd),
uintptr(unsafe.Pointer(oldp)),
uintptr(newdirfd),
uintptr(unsafe.Pointer(newp)),
uintptr(flag),
0)
if errno != 0 {
return errno
}
return nil
}

View File

@ -7,6 +7,7 @@ package unix
//go:cgo_import_dynamic libc_fchmodat fchmodat "libc.a/shr_64.o"
//go:cgo_import_dynamic libc_fchownat fchownat "libc.a/shr_64.o"
//go:cgo_import_dynamic libc_fstatat fstatat "libc.a/shr_64.o"
//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_unlinkat unlinkat "libc.a/shr_64.o"

View File

@ -128,3 +128,29 @@ func Renameat(olddirfd int, oldpath string, newdirfd int, newpath string) error
}
return nil
}
func libc_linkat_trampoline()
//go:cgo_import_dynamic libc_linkat linkat "/usr/lib/libSystem.B.dylib"
func Linkat(olddirfd int, oldpath string, newdirfd int, newpath string, flag int) 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_linkat_trampoline),
uintptr(olddirfd),
uintptr(unsafe.Pointer(oldp)),
uintptr(newdirfd),
uintptr(unsafe.Pointer(newp)),
uintptr(flag),
0)
if errno != 0 {
return errno
}
return nil
}

View File

@ -19,6 +19,7 @@ import (
//go:linkname procFchmodat libc_fchmodat
//go:linkname procFchownat libc_fchownat
//go:linkname procRenameat libc_renameat
//go:linkname procLinkat libc_linkat
var (
procFstatat,
@ -28,7 +29,8 @@ var (
procMkdirat,
procFchmodat,
procFchownat,
procRenameat uintptr
procRenameat,
procLinkat uintptr
)
func Unlinkat(dirfd int, path string, flags int) error {
@ -184,3 +186,25 @@ func Renameat(olddirfd int, oldpath string, newdirfd int, newpath string) error
}
return nil
}
func Linkat(olddirfd int, oldpath string, newdirfd int, newpath string, flag int) 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(&procLinkat)), 5,
uintptr(olddirfd),
uintptr(unsafe.Pointer(oldp)),
uintptr(newdirfd),
uintptr(unsafe.Pointer(newp)),
uintptr(flag),
0)
if errno != 0 {
return errno
}
return nil
}

View File

@ -119,3 +119,29 @@ func Renameat(olddirfd int, oldpath string, newdirfd int, newpath string) error
}
return nil
}
func libc_linkat_trampoline()
//go:cgo_import_dynamic libc_linkat linkat "libc.so"
func Linkat(olddirfd int, oldpath string, newdirfd int, newpath string, flag int) 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_linkat_trampoline),
uintptr(olddirfd),
uintptr(unsafe.Pointer(oldp)),
uintptr(newdirfd),
uintptr(unsafe.Pointer(newp)),
uintptr(flag),
0)
if errno != 0 {
return errno
}
return nil
}

View File

@ -16,6 +16,7 @@ func rawSyscall6(trap, nargs, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, e
//go:cgo_import_dynamic libc_fchmodat fchmodat "libc.so"
//go:cgo_import_dynamic libc_fchownat fchownat "libc.so"
//go:cgo_import_dynamic libc_fstatat fstatat "libc.so"
//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_unlinkat unlinkat "libc.so"

View File

@ -15,6 +15,7 @@ const (
fchmodatTrap uintptr = syscall.SYS_FCHMODAT
fchownatTrap uintptr = syscall.SYS_FCHOWNAT
renameatTrap uintptr = syscall.SYS_RENAMEAT
linkatTrap uintptr = syscall.SYS_LINKAT
AT_EACCESS = 0x4
AT_FDCWD = 0xfffafdcd

View File

@ -22,4 +22,5 @@ const (
fchmodatTrap uintptr = syscall.SYS_FCHMODAT
fchownatTrap uintptr = syscall.SYS_FCHOWNAT
renameatTrap uintptr = syscall.SYS_RENAMEAT
linkatTrap uintptr = syscall.SYS_LINKAT
)

View File

@ -13,6 +13,7 @@ const (
mkdiratTrap uintptr = syscall.SYS_MKDIRAT
fchmodatTrap uintptr = syscall.SYS_FCHMODAT
fchownatTrap uintptr = syscall.SYS_FCHOWNAT
linkatTrap uintptr = syscall.SYS_LINKAT
)
const (

View File

@ -15,6 +15,7 @@ const (
fchmodatTrap uintptr = syscall.SYS_FCHMODAT
fchownatTrap uintptr = syscall.SYS_FCHOWNAT
renameatTrap uintptr = syscall.SYS_RENAMEAT
linkatTrap uintptr = syscall.SYS_LINKAT
)
const (

View File

@ -129,6 +129,25 @@ func Renameat(olddirfd int, oldpath string, newdirfd int, newpath string) error
))
}
//go:wasmimport wasi_snapshot_preview1 path_link
//go:noescape
func path_link(oldFd int32, oldFlags uint32, 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 == "" {
return syscall.EINVAL
}
return errnoErr(path_link(
int32(olddirfd),
0,
unsafe.StringData(oldpath),
size(len(oldpath)),
int32(newdirfd),
unsafe.StringData(newpath),
size(len(newpath)),
))
}
//go:wasmimport wasi_snapshot_preview1 path_create_directory
//go:noescape
func path_create_directory(fd int32, path *byte, pathLen size) syscall.Errno

View File

@ -328,3 +328,51 @@ func Renameat(olddirfd syscall.Handle, oldpath string, newdirfd syscall.Handle,
}
return err
}
func Linkat(olddirfd syscall.Handle, oldpath string, newdirfd syscall.Handle, newpath string) error {
objAttrs := &OBJECT_ATTRIBUTES{}
if err := objAttrs.init(olddirfd, oldpath); err != nil {
return err
}
var h syscall.Handle
err := NtOpenFile(
&h,
SYNCHRONIZE|FILE_WRITE_ATTRIBUTES,
objAttrs,
&IO_STATUS_BLOCK{},
FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|FILE_SYNCHRONOUS_IO_NONALERT,
)
if err != nil {
return ntCreateFileError(err, 0)
}
defer syscall.CloseHandle(h)
linkInfo := FILE_LINK_INFORMATION{
RootDirectory: newdirfd,
}
p16, err := syscall.UTF16FromString(newpath)
if err != nil {
return err
}
if len(p16) > len(linkInfo.FileName) {
return syscall.EINVAL
}
copy(linkInfo.FileName[:], p16)
linkInfo.FileNameLength = uint32((len(p16) - 1) * 2)
const (
FileLinkInformation = 11
)
err = NtSetInformationFile(
h,
&IO_STATUS_BLOCK{},
uintptr(unsafe.Pointer(&linkInfo)),
uint32(unsafe.Sizeof(FILE_LINK_INFORMATION{})),
FileLinkInformation,
)
if st, ok := err.(NTStatus); ok {
return st.Errno()
}
return err
}

View File

@ -238,3 +238,11 @@ type FILE_RENAME_INFORMATION_EX struct {
FileNameLength uint32
FileName [syscall.MAX_PATH]uint16
}
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_link_information
type FILE_LINK_INFORMATION struct {
ReplaceIfExists bool
RootDirectory syscall.Handle
FileNameLength uint32
FileName [syscall.MAX_PATH]uint16
}

View File

@ -850,34 +850,49 @@ func TestReaddirOfFile(t *testing.T) {
}
func TestHardLink(t *testing.T) {
testMaybeRooted(t, testHardLink)
}
func testHardLink(t *testing.T, root *Root) {
testenv.MustHaveLink(t)
t.Chdir(t.TempDir())
var (
create = Create
link = Link
stat = Stat
op = "link"
)
if root != nil {
create = root.Create
link = root.Link
stat = root.Stat
op = "linkat"
}
from, to := "hardlinktestfrom", "hardlinktestto"
file, err := Create(to)
file, err := create(to)
if err != nil {
t.Fatalf("open %q failed: %v", to, err)
}
if err = file.Close(); err != nil {
t.Errorf("close %q failed: %v", to, err)
}
err = Link(to, from)
err = link(to, from)
if err != nil {
t.Fatalf("link %q, %q failed: %v", to, from, err)
}
none := "hardlinktestnone"
err = Link(none, none)
err = link(none, none)
// Check the returned error is well-formed.
if lerr, ok := err.(*LinkError); !ok || lerr.Error() == "" {
t.Errorf("link %q, %q failed to return a valid error", none, none)
}
tostat, err := Stat(to)
tostat, err := stat(to)
if err != nil {
t.Fatalf("stat %q failed: %v", to, err)
}
fromstat, err := Stat(from)
fromstat, err := stat(from)
if err != nil {
t.Fatalf("stat %q failed: %v", from, err)
}
@ -885,11 +900,11 @@ func TestHardLink(t *testing.T) {
t.Errorf("link %q, %q did not create hard link", to, from)
}
// We should not be able to perform the same Link() a second time
err = Link(to, from)
err = link(to, from)
switch err := err.(type) {
case *LinkError:
if err.Op != "link" {
t.Errorf("Link(%q, %q) err.Op = %q; want %q", to, from, err.Op, "link")
if err.Op != op {
t.Errorf("Link(%q, %q) err.Op = %q; want %q", to, from, err.Op, op)
}
if err.Old != to {
t.Errorf("Link(%q, %q) err.Old = %q; want %q", to, from, err.Old, to)

View File

@ -206,6 +206,18 @@ func (r *Root) Rename(oldname, newname string) error {
return rootRename(r, oldname, newname)
}
// Link creates newname as a hard link to the oldname file.
// Both paths are relative to the root.
// See [Link] for more details.
//
// If oldname is a symbolic link, Link creates new link to oldname and not its target.
// This behavior may differ from that of [Link] on some platforms.
//
// When GOOS=js, Link returns an error if oldname is a symbolic link.
func (r *Root) Link(oldname, newname string) error {
return rootLink(r, oldname, newname)
}
func (r *Root) logOpen(name string) {
if log := testlog.Logger(); log != nil {
// This won't be right if r's name has changed since it was opened,

View File

@ -180,3 +180,21 @@ func rootRename(r *Root, oldname, newname string) error {
}
return nil
}
func rootLink(r *Root, oldname, newname string) error {
if err := checkPathEscapesLstat(r, oldname); err != nil {
return &PathError{Op: "linkat", Path: oldname, Err: err}
}
fullOldName := joinPath(r.root.name, oldname)
if fs, err := Lstat(fullOldName); err == nil && fs.Mode()&ModeSymlink != 0 {
return &PathError{Op: "linkat", Path: oldname, Err: errors.New("cannot create a hard link to a symlink")}
}
if err := checkPathEscapesLstat(r, newname); err != nil {
return &PathError{Op: "linkat", Path: newname, Err: err}
}
err := Link(fullOldName, joinPath(r.root.name, newname))
if err != nil {
return &LinkError{"linkat", oldname, newname, underlyingError(err)}
}
return nil
}

View File

@ -151,6 +151,19 @@ func rootRename(r *Root, oldname, newname string) error {
return err
}
func rootLink(r *Root, oldname, newname string) error {
_, err := doInRoot(r, oldname, func(oldparent sysfdType, oldname string) (struct{}, error) {
_, err := doInRoot(r, newname, func(newparent sysfdType, newname string) (struct{}, error) {
return struct{}{}, linkat(oldparent, oldname, newparent, newname)
})
return struct{}{}, err
})
if err != nil {
return &LinkError{"linkat", oldname, newname, err}
}
return err
}
// doInRoot performs an operation on a path in a Root.
//
// It opens the directory containing the final element of the path,

View File

@ -8,6 +8,7 @@ import (
"bytes"
"errors"
"fmt"
"internal/testenv"
"io"
"io/fs"
"net"
@ -701,6 +702,16 @@ func TestRootReadlink(t *testing.T) {
// TestRootRenameFrom tests renaming the test case target to a known-good path.
func TestRootRenameFrom(t *testing.T) {
testRootMoveFrom(t, true)
}
// TestRootRenameFrom tests linking the test case target to a known-good path.
func TestRootLinkFrom(t *testing.T) {
testenv.MustHaveLink(t)
testRootMoveFrom(t, false)
}
func testRootMoveFrom(t *testing.T, rename bool) {
want := []byte("target")
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
@ -719,6 +730,11 @@ func TestRootRenameFrom(t *testing.T) {
if err != nil {
t.Fatalf("root.Readlink(%q) = %v, want success", test.ltarget, err)
}
// When GOOS=js, creating a hard link to a symlink fails.
if !rename && runtime.GOOS == "js" {
wantError = true
}
}
const dstPath = "destination"
@ -728,28 +744,67 @@ func TestRootRenameFrom(t *testing.T) {
wantError = true
}
err := root.Rename(test.open, dstPath)
if errEndsTest(t, err, wantError, "root.Rename(%q, %q)", test.open, dstPath) {
var op string
var err error
if rename {
op = "Rename"
err = root.Rename(test.open, dstPath)
} else {
op = "Link"
err = root.Link(test.open, dstPath)
}
if errEndsTest(t, err, wantError, "root.%v(%q, %q)", op, test.open, dstPath) {
return
}
origPath := target
if test.ltarget != "" {
got, err := os.Readlink(filepath.Join(root.Name(), dstPath))
if err != nil || got != linkTarget {
t.Errorf("os.Readlink(%q) = %q, %v, want %q", dstPath, got, err, linkTarget)
origPath = filepath.Join(root.Name(), test.ltarget)
}
_, err = os.Lstat(origPath)
if rename {
if !errors.Is(err, os.ErrNotExist) {
t.Errorf("after renaming file, Lstat(%q) = %v, want ErrNotExist", origPath, err)
}
} else {
got, err := os.ReadFile(filepath.Join(root.Name(), dstPath))
if err != nil || !bytes.Equal(got, want) {
t.Errorf(`os.ReadFile(%q): read content %q, %v; want %q`, dstPath, string(got), err, string(want))
if err != nil {
t.Errorf("after linking file, error accessing original: %v", err)
}
}
dstFullPath := filepath.Join(root.Name(), dstPath)
if test.ltarget != "" {
got, err := os.Readlink(dstFullPath)
if err != nil || got != linkTarget {
t.Errorf("os.Readlink(%q) = %q, %v, want %q", dstFullPath, got, err, linkTarget)
}
} else {
got, err := os.ReadFile(dstFullPath)
if err != nil || !bytes.Equal(got, want) {
t.Errorf(`os.ReadFile(%q): read content %q, %v; want %q`, dstFullPath, string(got), err, string(want))
}
st, err := os.Lstat(dstFullPath)
if err != nil || st.Mode()&fs.ModeSymlink != 0 {
t.Errorf(`os.Lstat(%q) = %v, %v; want non-symlink`, dstFullPath, st.Mode(), err)
}
}
})
}
}
// TestRootRenameTo tests renaming a known-good path to the test case target.
func TestRootRenameTo(t *testing.T) {
testRootMoveTo(t, true)
}
// TestRootLinkTo tests renaming a known-good path to the test case target.
func TestRootLinkTo(t *testing.T) {
testenv.MustHaveLink(t)
testRootMoveTo(t, true)
}
func testRootMoveTo(t *testing.T, rename bool) {
want := []byte("target")
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
@ -771,11 +826,30 @@ func TestRootRenameTo(t *testing.T) {
wantError = true
}
err := root.Rename(srcPath, test.open)
if errEndsTest(t, err, wantError, "root.Rename(%q, %q)", srcPath, test.open) {
var err error
var op string
if rename {
op = "Rename"
err = root.Rename(srcPath, test.open)
} else {
op = "Link"
err = root.Link(srcPath, test.open)
}
if errEndsTest(t, err, wantError, "root.%v(%q, %q)", op, srcPath, test.open) {
return
}
_, err = os.Lstat(filepath.Join(root.Name(), srcPath))
if rename {
if !errors.Is(err, os.ErrNotExist) {
t.Errorf("after renaming file, Lstat(%q) = %v, want ErrNotExist", srcPath, err)
}
} else {
if err != nil {
t.Errorf("after linking file, error accessing original: %v", err)
}
}
got, err := os.ReadFile(filepath.Join(root.Name(), target))
if err != nil || !bytes.Equal(got, want) {
t.Errorf(`os.ReadFile(%q): read content %q, %v; want %q`, target, string(got), err, string(want))
@ -1201,6 +1275,15 @@ func TestRootConsistencyReadlink(t *testing.T) {
}
func TestRootConsistencyRename(t *testing.T) {
testRootConsistencyMove(t, true)
}
func TestRootConsistencyLink(t *testing.T) {
testenv.MustHaveLink(t)
testRootConsistencyMove(t, false)
}
func testRootConsistencyMove(t *testing.T, rename bool) {
if runtime.GOOS == "plan9" {
// This test depends on moving files between directories.
t.Skip("Plan 9 does not support cross-directory renames")
@ -1222,10 +1305,19 @@ func TestRootConsistencyRename(t *testing.T) {
}
test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
rename := os.Rename
var move func(oldname, newname string) error
switch {
case rename && r == nil:
move = os.Rename
case rename && r != nil:
move = r.Rename
case !rename && r == nil:
move = os.Link
case !rename && r != nil:
move = r.Link
}
lstat := os.Lstat
if r != nil {
rename = r.Rename
lstat = r.Lstat
}
@ -1243,7 +1335,21 @@ func TestRootConsistencyRename(t *testing.T) {
dstPath = path
}
if err := rename(srcPath, dstPath); err != nil {
if !rename {
// When the source is a symlink, Root.Link creates
// a hard link to the symlink.
// os.Link does whatever the link syscall does,
// which varies between operating systems and
// their versions.
// Skip running the consistency test when
// the source is a symlink.
fi, err := lstat(srcPath)
if err == nil && fi.Mode()&os.ModeSymlink != 0 {
return "", nil
}
}
if err := move(srcPath, dstPath); err != nil {
return "", err
}
fi, err := lstat(dstPath)

View File

@ -213,6 +213,10 @@ func renameat(oldfd int, oldname string, newfd int, newname string) error {
return unix.Renameat(oldfd, oldname, newfd, newname)
}
func linkat(oldfd int, oldname string, newfd int, newname string) error {
return unix.Linkat(oldfd, oldname, newfd, newname, 0)
}
// checkSymlink resolves the symlink name in parent,
// and returns errSymlink with the link contents.
//

View File

@ -319,6 +319,10 @@ func renameat(oldfd syscall.Handle, oldname string, newfd syscall.Handle, newnam
return windows.Renameat(oldfd, oldname, newfd, newname)
}
func linkat(oldfd syscall.Handle, oldname string, newfd syscall.Handle, newname string) error {
return windows.Linkat(oldfd, oldname, newfd, newname)
}
func readlinkat(dirfd syscall.Handle, name string) (string, error) {
fd, err := openat(dirfd, name, windows.O_OPEN_REPARSE, 0)
if err != nil {