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
* [os.Root.Chmod]
* [os.Root.Chown]
+ * [os.Root.Chtimes]
--- /dev/null
+// 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
--- /dev/null
+// 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)
+}
// 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() {
}
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,
"io/fs"
"runtime"
"slices"
+ "time"
)
// OpenInRoot opens the file name in the directory dir.
//
// - 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.
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 {
import (
"errors"
"sync/atomic"
+ "time"
)
// root implementation for platforms with no openat.
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}
"slices"
"sync"
"syscall"
+ "time"
)
// root implementation for platforms with a function to open a file
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)
}
}
+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) {
"internal/syscall/unix"
"runtime"
"syscall"
+ "time"
)
type sysfdType = int
})
}
+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))
"internal/syscall/windows"
"runtime"
"syscall"
+ "time"
"unsafe"
)
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)
+}