Add support for FUSE_COPY_FILE_RANGE_64

This commit is contained in:
Antonio SJ Musumeci 2026-04-01 09:48:12 -05:00
parent 1ebc8cfa5f
commit 041ece5d09
7 changed files with 160 additions and 41 deletions

View file

@ -71,6 +71,52 @@ def main():
os.close(mdfd)
os.close(ndfd)
# Exercise very large copy lengths to validate the 64-bit copy result
# path when available. Use sparse files to keep runtime and disk usage low.
large_len = (5 << 30) + 4096
merge_large_src = join(mount, "posix-cfr/src-large")
merge_large_dst = join(mount, "posix-cfr/dst-large")
native_large_src = join(native, "posix-cfr/src-large")
native_large_dst = join(native, "posix-cfr/dst-large")
touch(merge_large_src, b"")
touch(merge_large_dst, b"")
touch(native_large_src, b"")
touch(native_large_dst, b"")
msfd = os.open(merge_large_src, os.O_RDWR)
mdfd = os.open(merge_large_dst, os.O_RDWR)
nsfd = os.open(native_large_src, os.O_RDWR)
ndfd = os.open(native_large_dst, os.O_RDWR)
try:
# Make sparse files with one non-zero byte at the end so both sides
# have comparable logical contents.
os.ftruncate(msfd, large_len)
os.ftruncate(nsfd, large_len)
os.pwrite(msfd, b"z", large_len - 1)
os.pwrite(nsfd, b"z", large_len - 1)
err = compare_calls(
"copy_file_range large sparse",
lambda: os.copy_file_range(msfd, mdfd, large_len),
lambda: os.copy_file_range(nsfd, ndfd, large_len),
lambda a, b: a == b,
)
if err:
return fail(err)
mstat = os.fstat(mdfd)
nstat = os.fstat(ndfd)
if mstat.st_size != nstat.st_size:
return fail(
"copy_file_range large sparse dst size mismatch "
f"mergerfs={mstat.st_size} native={nstat.st_size}"
)
finally:
os.close(msfd)
os.close(mdfd)
os.close(nsfd)
os.close(ndfd)
return 0

View file

@ -2,11 +2,15 @@
#include <stdint.h>
typedef uint16_t u16;
typedef int16_t s16;
typedef uint32_t u32;
typedef int32_t s32;
typedef uint64_t u64;
typedef int64_t s64;
typedef const uint16_t cu16;
typedef const int16_t cs16;
typedef const uint32_t cu32;
typedef const int32_t cs32;
typedef const uint64_t cu64;

View file

@ -229,6 +229,17 @@
* - FUSE_URING_IN_OUT_HEADER_SZ
* - FUSE_URING_OP_IN_OUT_SZ
* - enum fuse_uring_cmd
*
* 7.43
* - add FUSE_REQUEST_TIMEOUT
*
* 7.44
* - add FUSE_NOTIFY_INC_EPOCH
*
* 7.45
* - add FUSE_COPY_FILE_RANGE_64
* - add struct fuse_copy_file_range_out
* - add FUSE_NOTIFY_PRUNE
*/
#ifndef _LINUX_FUSE_H
@ -264,7 +275,7 @@
#define FUSE_KERNEL_VERSION 7
/** Minor version number of this interface */
#define FUSE_KERNEL_MINOR_VERSION 42
#define FUSE_KERNEL_MINOR_VERSION 45
/** The node ID of the root inode */
#define FUSE_ROOT_ID 1
@ -435,39 +446,41 @@ struct fuse_file_lock {
* of the request ID indicates resend requests
* FUSE_ALLOW_IDMAP: allow creation of idmapped mounts
* FUSE_OVER_IO_URING: Indicate that client supports io-uring
* FUSE_REQUEST_TIMEOUT: kernel supports timing out requests.
* init_out.request_timeout contains the timeout (in secs)
*/
#define FUSE_ASYNC_READ (1ULL << 0)
#define FUSE_POSIX_LOCKS (1ULL << 1)
#define FUSE_FILE_OPS (1ULL << 2)
#define FUSE_ATOMIC_O_TRUNC (1ULL << 3)
#define FUSE_EXPORT_SUPPORT (1ULL << 4)
#define FUSE_BIG_WRITES (1ULL << 5)
#define FUSE_DONT_MASK (1ULL << 6)
#define FUSE_SPLICE_WRITE (1ULL << 7)
#define FUSE_SPLICE_MOVE (1ULL << 8)
#define FUSE_SPLICE_READ (1ULL << 9)
#define FUSE_FLOCK_LOCKS (1ULL << 10)
#define FUSE_HAS_IOCTL_DIR (1ULL << 11)
#define FUSE_AUTO_INVAL_DATA (1ULL << 12)
#define FUSE_DO_READDIRPLUS (1ULL << 13)
#define FUSE_READDIRPLUS_AUTO (1ULL << 14)
#define FUSE_ASYNC_DIO (1ULL << 15)
#define FUSE_WRITEBACK_CACHE (1ULL << 16)
#define FUSE_NO_OPEN_SUPPORT (1ULL << 17)
#define FUSE_PARALLEL_DIROPS (1ULL << 18)
#define FUSE_HANDLE_KILLPRIV (1ULL << 19)
#define FUSE_POSIX_ACL (1ULL << 20)
#define FUSE_ABORT_ERROR (1ULL << 21)
#define FUSE_MAX_PAGES (1ULL << 22)
#define FUSE_CACHE_SYMLINKS (1ULL << 23)
#define FUSE_NO_OPENDIR_SUPPORT (1ULL << 24)
#define FUSE_EXPLICIT_INVAL_DATA (1ULL << 25)
#define FUSE_MAP_ALIGNMENT (1ULL << 26)
#define FUSE_SUBMOUNTS (1ULL << 27)
#define FUSE_HANDLE_KILLPRIV_V2 (1ULL << 28)
#define FUSE_SETXATTR_EXT (1ULL << 29)
#define FUSE_INIT_EXT (1ULL << 30)
#define FUSE_INIT_RESERVED (1ULL << 31)
#define FUSE_ASYNC_READ (1 << 0)
#define FUSE_POSIX_LOCKS (1 << 1)
#define FUSE_FILE_OPS (1 << 2)
#define FUSE_ATOMIC_O_TRUNC (1 << 3)
#define FUSE_EXPORT_SUPPORT (1 << 4)
#define FUSE_BIG_WRITES (1 << 5)
#define FUSE_DONT_MASK (1 << 6)
#define FUSE_SPLICE_WRITE (1 << 7)
#define FUSE_SPLICE_MOVE (1 << 8)
#define FUSE_SPLICE_READ (1 << 9)
#define FUSE_FLOCK_LOCKS (1 << 10)
#define FUSE_HAS_IOCTL_DIR (1 << 11)
#define FUSE_AUTO_INVAL_DATA (1 << 12)
#define FUSE_DO_READDIRPLUS (1 << 13)
#define FUSE_READDIRPLUS_AUTO (1 << 14)
#define FUSE_ASYNC_DIO (1 << 15)
#define FUSE_WRITEBACK_CACHE (1 << 16)
#define FUSE_NO_OPEN_SUPPORT (1 << 17)
#define FUSE_PARALLEL_DIROPS (1 << 18)
#define FUSE_HANDLE_KILLPRIV (1 << 19)
#define FUSE_POSIX_ACL (1 << 20)
#define FUSE_ABORT_ERROR (1 << 21)
#define FUSE_MAX_PAGES (1 << 22)
#define FUSE_CACHE_SYMLINKS (1 << 23)
#define FUSE_NO_OPENDIR_SUPPORT (1 << 24)
#define FUSE_EXPLICIT_INVAL_DATA (1 << 25)
#define FUSE_MAP_ALIGNMENT (1 << 26)
#define FUSE_SUBMOUNTS (1 << 27)
#define FUSE_HANDLE_KILLPRIV_V2 (1 << 28)
#define FUSE_SETXATTR_EXT (1 << 29)
#define FUSE_INIT_EXT (1 << 30)
#define FUSE_INIT_RESERVED (1U << 31)
/* bits 32..63 get shifted down 32 bits into the flags2 field */
#define FUSE_SECURITY_CTX (1ULL << 32)
#define FUSE_HAS_INODE_DAX (1ULL << 33)
@ -477,11 +490,11 @@ struct fuse_file_lock {
#define FUSE_PASSTHROUGH (1ULL << 37)
#define FUSE_NO_EXPORT_SUPPORT (1ULL << 38)
#define FUSE_HAS_RESEND (1ULL << 39)
/* Obsolete alias for FUSE_DIRECT_IO_ALLOW_MMAP */
#define FUSE_DIRECT_IO_RELAX FUSE_DIRECT_IO_ALLOW_MMAP
#define FUSE_ALLOW_IDMAP (1ULL << 40)
#define FUSE_OVER_IO_URING (1ULL << 41)
#define FUSE_REQUEST_TIMEOUT (1ULL << 42)
/**
* CUSE INIT request/reply flags
@ -649,6 +662,7 @@ enum fuse_opcode {
FUSE_SYNCFS = 50,
FUSE_TMPFILE = 51,
FUSE_STATX = 52,
FUSE_COPY_FILE_RANGE_64 = 53,
/* CUSE specific operations */
CUSE_INIT = 4096,
@ -666,7 +680,8 @@ enum fuse_notify_code {
FUSE_NOTIFY_RETRIEVE = 5,
FUSE_NOTIFY_DELETE = 6,
FUSE_NOTIFY_RESEND = 7,
FUSE_NOTIFY_CODE_MAX,
FUSE_NOTIFY_INC_EPOCH = 8,
FUSE_NOTIFY_PRUNE = 9,
};
/* The read buffer is required to be at least 8k, but may be much larger */
@ -909,7 +924,8 @@ struct fuse_init_out {
uint16_t map_alignment;
uint32_t flags2;
uint32_t max_stack_depth;
uint32_t unused[6];
uint16_t request_timeout;
uint16_t unused[11];
};
#define CUSE_INIT_INFO_MAX 4096
@ -1104,6 +1120,12 @@ struct fuse_notify_retrieve_in {
uint64_t dummy4;
};
struct fuse_notify_prune_out {
uint32_t count;
uint32_t padding;
uint64_t spare;
};
struct fuse_backing_map {
int32_t fd;
uint32_t flags;
@ -1116,6 +1138,7 @@ struct fuse_backing_map {
#define FUSE_DEV_IOC_BACKING_OPEN _IOW(FUSE_DEV_IOC_MAGIC, 1, \
struct fuse_backing_map)
#define FUSE_DEV_IOC_BACKING_CLOSE _IOW(FUSE_DEV_IOC_MAGIC, 2, uint32_t)
#define FUSE_DEV_IOC_SYNC_INIT _IO(FUSE_DEV_IOC_MAGIC, 3)
struct fuse_lseek_in {
uint64_t fh;
@ -1138,6 +1161,11 @@ struct fuse_copy_file_range_in {
uint64_t flags;
};
/* For FUSE_COPY_FILE_RANGE_64 */
struct fuse_copy_file_range_out {
uint64_t bytes_copied;
};
#define FUSE_SETUPMAPPING_FLAG_WRITE (1ull << 0)
#define FUSE_SETUPMAPPING_FLAG_READ (1ull << 1)
struct fuse_setupmapping_in {

View file

@ -271,6 +271,8 @@ int fuse_reply_open(fuse_req_t *req,
*/
int fuse_reply_write(fuse_req_t *req, size_t count);
int fuse_reply_copy_file_range_64(fuse_req_t *req, size_t count);
/**
* Reply with data
*

View file

@ -226,7 +226,7 @@ _open_flag_to_str(const uint64_t offset_)
#undef OPEN_FLAG_CASE
#define FUSE_INIT_FLAG_CASE(X) case FUSE_##X: return #X
#define FUSE_INIT_FLAG_CASE(X) case static_cast<uint64_t>(FUSE_##X): return #X
static
const
@ -277,6 +277,7 @@ _fuse_init_flag_name(const uint64_t flag_)
FUSE_INIT_FLAG_CASE(HAS_RESEND);
FUSE_INIT_FLAG_CASE(ALLOW_IDMAP);
FUSE_INIT_FLAG_CASE(OVER_IO_URING);
FUSE_INIT_FLAG_CASE(REQUEST_TIMEOUT);
}
return nullptr;
@ -1493,7 +1494,8 @@ _opcode_name(enum fuse_opcode op_)
"REMOVEMAPPING",
"SYNCFS",
"TMPFILE",
"STATX"
"STATX",
"COPY_FILE_RANGE_64"
};
if(op_ >= (sizeof(names) / sizeof(names[0])))
@ -1653,6 +1655,7 @@ fuse_debug_in_header(const struct fuse_in_header *hdr_)
_debug_fuse_lseek_in(arg);
break;
case FUSE_COPY_FILE_RANGE:
case FUSE_COPY_FILE_RANGE_64:
_debug_fuse_copy_file_range_in(arg);
break;
case FUSE_TMPFILE:

View file

@ -2711,8 +2711,17 @@ fuse_lib_copy_file_range(fuse_req_t *req_,
fuse_file_info_t ffi_in = {};
fuse_file_info_t ffi_out = {};
const struct fuse_copy_file_range_in *arg;
struct fuse_copy_file_range_in arg_tmp;
arg = (fuse_copy_file_range_in*)fuse_hdr_arg(hdr_);
if(((hdr_->opcode == FUSE_COPY_FILE_RANGE) || (sizeof(size_t) == 4)) &&
(arg->len > 0xfffff000ULL))
{
/* Avoid truncation in the old reply format or 32-bit size_t conversion. */
arg_tmp = *arg;
arg_tmp.len = 0xfffff000ULL;
arg = &arg_tmp;
}
ffi_in.fh = arg->fh_in;
ffi_out.fh = arg->fh_out;
@ -2725,7 +2734,12 @@ fuse_lib_copy_file_range(fuse_req_t *req_,
arg->flags);
if(rv >= 0)
fuse_reply_write(req_,rv);
{
if(hdr_->opcode == FUSE_COPY_FILE_RANGE_64)
fuse_reply_copy_file_range_64(req_,rv);
else
fuse_reply_write(req_,rv);
}
else
fuse_reply_err(req_,rv);
}

View file

@ -395,6 +395,17 @@ fuse_reply_write(fuse_req_t *req,
return send_reply_ok(req, &arg, sizeof(arg));
}
int
fuse_reply_copy_file_range_64(fuse_req_t *req,
size_t count)
{
struct fuse_copy_file_range_out arg = {};
arg.bytes_copied = count;
return send_reply_ok(req, &arg, sizeof(arg));
}
int
fuse_reply_buf(fuse_req_t *req,
const char *buf,
@ -1359,6 +1370,14 @@ do_copy_file_range(fuse_req_t *req_,
f.op.copy_file_range(req_,hdr_);
}
static
void
do_copy_file_range_64(fuse_req_t *req_,
struct fuse_in_header *hdr_)
{
f.op.copy_file_range(req_,hdr_);
}
static
void
do_setupmapping(fuse_req_t *req_,
@ -1602,7 +1621,7 @@ fuse_lowlevel_notify_retrieve(struct fuse_session *se,
return err;
}
#define FUSE_OPCODE_LEN (FUSE_STATX + 1)
#define FUSE_OPCODE_LEN (FUSE_COPY_FILE_RANGE_64 + 1)
typedef void (*fuse_ll_func)(fuse_req_t*, struct fuse_in_header *);
const
@ -1661,7 +1680,8 @@ fuse_ll_funcs[FUSE_OPCODE_LEN] =
do_removemapping,
do_syncfs,
do_tmpfile,
do_statx
do_statx,
do_copy_file_range_64
};
#define FUSE_MAXOPS (sizeof(fuse_ll_funcs) / sizeof(fuse_ll_funcs[0]))
@ -1757,6 +1777,7 @@ fuse_ll_buf_process_read(struct fuse_session *se_,
req->ctx.umask = 0;
req->conn = f.conn;
req->se = se_;
req->ioctl_64bit = 0;
err = ENOSYS;
if(in->opcode >= FUSE_MAXOPS)
@ -1795,6 +1816,7 @@ fuse_ll_buf_process_read_init(struct fuse_session *se_,
req->ctx.opcode = in->opcode;
req->ctx.unique = in->unique;
req->ctx.nodeid = in->nodeid;
req->ioctl_64bit = 0;
req->ctx.uid = in->uid;
req->ctx.gid = in->gid;
req->ctx.pid = in->pid;