os: add Root.Chtimes

For #67002

Change-Id: I9b10ac30f852052c85d6d21eb1752a9de5474346
Reviewed-on: https://go-review.googlesource.com/c/go/+/649515
Auto-Submit: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Kirill Kolyshkin <kolyshkin@gmail.com>
Reviewed-by: Ian Lance Taylor <iant@google.com>
This commit is contained in:
Damien Neil 2025-02-11 10:47:20 -08:00 committed by Gopher Robot
parent 3309658d39
commit 2148309963
11 changed files with 178 additions and 5 deletions

View File

@ -1,2 +1,3 @@
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

View File

@ -2,3 +2,4 @@ The [os.Root] type supports the following additional methods:
* [os.Root.Chmod]
* [os.Root.Chown]
* [os.Root.Chtimes]

View File

@ -0,0 +1,15 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build unix && !wasip1
package unix
import (
"syscall"
_ "unsafe" // for //go:linkname
)
//go:linkname Utimensat syscall.utimensat
func Utimensat(dirfd int, path string, times *[2]syscall.Timespec, flag int) error

View File

@ -0,0 +1,42 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build wasip1
package unix
import (
"syscall"
"unsafe"
)
//go:wasmimport wasi_snapshot_preview1 path_filestat_set_times
//go:noescape
func path_filestat_set_times(fd int32, flags uint32, path *byte, pathLen size, atim uint64, mtim uint64, fstflags uint32) syscall.Errno
func Utimensat(dirfd int, path string, times *[2]syscall.Timespec, flag int) error {
if path == "" {
return syscall.EINVAL
}
atime := syscall.TimespecToNsec(times[0])
mtime := syscall.TimespecToNsec(times[1])
var fflag uint32
if times[0].Nsec != UTIME_OMIT {
fflag |= syscall.FILESTAT_SET_ATIM
}
if times[1].Nsec != UTIME_OMIT {
fflag |= syscall.FILESTAT_SET_MTIM
}
errno := path_filestat_set_times(
int32(dirfd),
syscall.LOOKUP_SYMLINK_FOLLOW,
unsafe.StringData(path),
size(len(path)),
uint64(atime),
uint64(mtime),
fflag,
)
return errnoErr(errno)
}

View File

@ -177,6 +177,14 @@ func (f *File) Sync() error {
// less precise time unit.
// If there is an error, it will be of type [*PathError].
func Chtimes(name string, atime time.Time, mtime time.Time) error {
utimes := chtimesUtimes(atime, mtime)
if e := syscall.UtimesNano(fixLongPath(name), utimes[0:]); e != nil {
return &PathError{Op: "chtimes", Path: name, Err: e}
}
return nil
}
func chtimesUtimes(atime, mtime time.Time) [2]syscall.Timespec {
var utimes [2]syscall.Timespec
set := func(i int, t time.Time) {
if t.IsZero() {
@ -187,10 +195,7 @@ func Chtimes(name string, atime time.Time, mtime time.Time) error {
}
set(0, atime)
set(1, mtime)
if e := syscall.UtimesNano(fixLongPath(name), utimes[0:]); e != nil {
return &PathError{Op: "chtimes", Path: name, Err: e}
}
return nil
return utimes
}
// Chdir changes the current working directory to the file,

View File

@ -12,6 +12,7 @@ import (
"io/fs"
"runtime"
"slices"
"time"
)
// OpenInRoot opens the file name in the directory dir.
@ -54,7 +55,7 @@ func OpenInRoot(dir, name string) (*File, error) {
//
// - When GOOS=windows, file names may not reference Windows reserved device names
// such as NUL and COM1.
// - On Unix, [Root.Chmod] and [Root.Chown] are vulnerable to a race condition.
// - On Unix, [Root.Chmod], [Root.Chown], and [Root.Chtimes] are vulnerable to a race condition.
// If the target of the operation is changed from a regular file to a symlink
// while the operation is in progress, the operation may be peformed on the link
// rather than the link target.
@ -158,6 +159,12 @@ func (r *Root) Chown(name string, uid, gid int) error {
return rootChown(r, name, uid, gid)
}
// Chtimes changes the access and modification times of the named file in the root.
// See [Chtimes] for more details.
func (r *Root) Chtimes(name string, atime time.Time, mtime time.Time) error {
return rootChtimes(r, name, atime, mtime)
}
// Remove removes the named file or (empty) directory in the root.
// See [Remove] for more details.
func (r *Root) Remove(name string) error {

View File

@ -9,6 +9,7 @@ package os
import (
"errors"
"sync/atomic"
"time"
)
// root implementation for platforms with no openat.
@ -115,6 +116,16 @@ func rootChown(r *Root, name string, uid, gid int) error {
return nil
}
func rootChtimes(r *Root, name string, atime time.Time, mtime time.Time) error {
if err := checkPathEscapes(r, name); err != nil {
return &PathError{Op: "chtimesat", Path: name, Err: err}
}
if err := Chtimes(joinPath(r.root.name, name), atime, mtime); err != nil {
return &PathError{Op: "chtimesat", Path: name, Err: underlyingError(err)}
}
return nil
}
func rootMkdir(r *Root, name string, perm FileMode) error {
if err := checkPathEscapes(r, name); err != nil {
return &PathError{Op: "mkdirat", Path: name, Err: err}

View File

@ -11,6 +11,7 @@ import (
"slices"
"sync"
"syscall"
"time"
)
// root implementation for platforms with a function to open a file
@ -87,6 +88,16 @@ func rootChown(r *Root, name string, uid, gid int) error {
return nil
}
func rootChtimes(r *Root, name string, atime time.Time, mtime time.Time) error {
_, err := doInRoot(r, name, func(parent sysfdType, name string) (struct{}, error) {
return struct{}{}, chtimesat(parent, name, atime, mtime)
})
if err != nil {
return &PathError{Op: "chtimesat", Path: name, Err: err}
}
return err
}
func rootMkdir(r *Root, name string, perm FileMode) error {
_, err := doInRoot(r, name, func(parent sysfdType, name string) (struct{}, error) {
return struct{}{}, mkdirat(parent, name, perm)

View File

@ -426,6 +426,53 @@ func TestRootChmod(t *testing.T) {
}
}
func TestRootChtimes(t *testing.T) {
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {
if target != "" {
if err := os.WriteFile(target, nil, 0o666); err != nil {
t.Fatal(err)
}
}
for _, times := range []struct {
atime, mtime time.Time
}{{
atime: time.Now().Add(-1 * time.Minute),
mtime: time.Now().Add(-1 * time.Minute),
}, {
atime: time.Now().Add(1 * time.Minute),
mtime: time.Now().Add(1 * time.Minute),
}, {
atime: time.Time{},
mtime: time.Now(),
}, {
atime: time.Now(),
mtime: time.Time{},
}} {
if runtime.GOOS == "js" {
times.atime = times.atime.Truncate(1 * time.Second)
times.mtime = times.mtime.Truncate(1 * time.Second)
}
err := root.Chtimes(test.open, times.atime, times.mtime)
if errEndsTest(t, err, test.wantError, "root.Chtimes(%q)", test.open) {
return
}
st, err := os.Stat(target)
if err != nil {
t.Fatalf("os.Stat(%q) = %v", target, err)
}
if got := st.ModTime(); !times.mtime.IsZero() && !got.Equal(times.mtime) {
t.Errorf("after root.Chtimes(%q, %v, %v): got mtime=%v, want %v", test.open, times.atime, times.mtime, got, times.mtime)
}
if got := os.Atime(st); !times.atime.IsZero() && !got.Equal(times.atime) {
t.Errorf("after root.Chtimes(%q, %v, %v): got atime=%v, want %v", test.open, times.atime, times.mtime, got, times.atime)
}
}
})
}
}
func TestRootMkdir(t *testing.T) {
for _, test := range rootTestCases {
test.run(t, func(t *testing.T, target string, root *os.Root) {

View File

@ -11,6 +11,7 @@ import (
"internal/syscall/unix"
"runtime"
"syscall"
"time"
)
type sysfdType = int
@ -165,6 +166,15 @@ func chownat(parent int, name string, uid, gid int) error {
})
}
func chtimesat(parent int, name string, atime time.Time, mtime time.Time) error {
return afterResolvingSymlink(parent, name, func() error {
return ignoringEINTR(func() error {
utimes := chtimesUtimes(atime, mtime)
return unix.Utimensat(parent, name, &utimes, unix.AT_SYMLINK_NOFOLLOW)
})
})
}
func mkdirat(fd int, name string, perm FileMode) error {
return ignoringEINTR(func() error {
return unix.Mkdirat(fd, name, syscallMode(perm))

View File

@ -13,6 +13,7 @@ import (
"internal/syscall/windows"
"runtime"
"syscall"
"time"
"unsafe"
)
@ -287,3 +288,25 @@ func mkdirat(dirfd syscall.Handle, name string, perm FileMode) error {
func removeat(dirfd syscall.Handle, name string) error {
return windows.Deleteat(dirfd, name)
}
func chtimesat(dirfd syscall.Handle, name string, atime time.Time, mtime time.Time) error {
h, err := windows.Openat(dirfd, name, syscall.O_CLOEXEC|windows.O_NOFOLLOW_ANY|windows.O_WRITE_ATTRS, 0)
if err == syscall.ELOOP || err == syscall.ENOTDIR {
if link, err := readReparseLinkAt(dirfd, name); err == nil {
return errSymlink(link)
}
}
if err != nil {
return err
}
defer syscall.CloseHandle(h)
a := syscall.Filetime{}
w := syscall.Filetime{}
if !atime.IsZero() {
a = syscall.NsecToFiletime(atime.UnixNano())
}
if !mtime.IsZero() {
w = syscall.NsecToFiletime(mtime.UnixNano())
}
return syscall.SetFileTime(h, nil, &a, &w)
}