Files
nfs/internal/nfsfs/clone_linux.go
T

113 lines
3.4 KiB
Go
Raw Normal View History

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
//go:build linux
package nfsfs
import (
"runtime"
"syscall"
"unsafe"
)
// The reflink ioctl of linux/fs.h, FICLONERANGE, and the syscall number
// of copy_file_range(2), which the standard library does not export on
// linux. FICLONERANGE is _IOW(0x94, 13, struct file_clone_range) with a
// 32 byte struct: (1<<30)|(32<<16)|(0x94<<8)|13. The copy_file_range
// numbers follow the kernel's syscall tables: 319 on amd64 and 286 on
// the asm generic table of arm64, riscv64 and loong64. An architecture
// outside the table answers unshareable, and the caller keeps its
// userspace path.
const ioctlFICLONERANGE = 0x4020940D
// fileCloneRange mirrors struct file_clone_range of linux/fs.h, the
// argument of FICLONERANGE.
type fileCloneRange struct {
srcFD int64
srcOffset uint64
srcLength uint64
destOffset uint64
}
// CloneRange makes the destination carry the source's bytes through the
// reflink of the filesystem, XFS, btrfs and ZFS among them. The
// descriptor cache hands out both ends, so a range cloned through cached
// descriptors stays as verified as any other read or write.
func (l *Local) CloneRange(src Handle, srcOff int64, dst Handle, dstOff int64, length int64) error {
sf, releaseSrc, err := l.dataFD(src, false)
if err != nil {
return err
}
defer releaseSrc()
df, releaseDst, err := l.dataFD(dst, true)
if err != nil {
return err
}
defer releaseDst()
cr := fileCloneRange{
srcFD: int64(sf.Fd()),
srcOffset: uint64(srcOff),
srcLength: uint64(length),
destOffset: uint64(dstOff),
}
// SAFETY: ioctl takes the address of exactly the 32 byte struct the
// FICLONERANGE command name carries; the kernel reads it and writes
// nothing through it.
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, df.Fd(), ioctlFICLONERANGE,
uintptr(unsafe.Pointer(&cr))); errno != 0 {
return errno
}
return nil
}
// CopyRange copies the bytes through copy_file_range(2), in chunks until
// the length is served. The syscall answers how much moved; a short move
// on the first call means the kernel refused for these files and the
// error travels to the caller's fallback.
func (l *Local) CopyRange(src Handle, srcOff int64, dst Handle, dstOff int64, length int64) error {
sf, releaseSrc, err := l.dataFD(src, false)
if err != nil {
return err
}
defer releaseSrc()
df, releaseDst, err := l.dataFD(dst, true)
if err != nil {
return err
}
defer releaseDst()
number := copyFileRangeSyscall()
if number == 0 {
return syscall.ENOSYS
}
var inOff, outOff int64 = srcOff, dstOff
for length > 0 {
chunk := min(length, 8<<20)
// SAFETY: the syscall copies from and to the file offsets behind
// the two pointers and updates them; both live across the call.
n, _, errno := syscall.Syscall6(number, sf.Fd(), uintptr(unsafe.Pointer(&inOff)),
df.Fd(), uintptr(unsafe.Pointer(&outOff)), uintptr(chunk), 0)
if errno != 0 {
return errno
}
if n == 0 {
return syscall.EINVAL
}
length -= int64(n)
}
return nil
}
// copyFileRangeSyscall answers the syscall number of copy_file_range on
// the architectures this project builds for linux, zero elsewhere.
func copyFileRangeSyscall() uintptr {
switch runtime.GOARCH {
case "amd64":
return 319
case "arm64", "riscv64", "loong64":
return 286
default:
return 0
}
}