Server-side file copying between smb shares on linux
I needed to do a simple task: copy a file between two SMB shares residing on the same server. It would be wasteful to transfer this file back and forth between server and client machine using the network, so why not use the server-side copy feature?
Trying regular 'cp' command copies the file across the network, so there must be some other way. After a little googling, I've found on Samba wiki (https://wiki.samba.org/index.php/Server-Side_Copy) that Linux in fact does support server-side copying. The site mentioned 'cp --reflink' - which did not work for me - along with a program called 'cloner' - which I could find for the life of me. It's time for a little digging.
Samba wiki mentions that one must issue FSCTL_SRV_COPYCHUNK_WRITE request on the server, by using CIFS_IOC_COPYCHUNK_FILE ioctl.
grep-ing the cifs source files in the kernel 'fs' directory for this particular ioctl return two entries:
$ grep CIFS_IOC_COPYCHUNK_FILE * -r cifs_ioctl.h:#define CIFS_IOC_COPYCHUNK_FILE _IOW(CIFS_IOCTL_MAGIC, 3, int) ioctl.c: case CIFS_IOC_COPYCHUNK_FILE:
_IOW is a macro defined in include/uapi/asm-generic/ioctl.h
This macro basically translates given arguments into a 32-bit long message which is sent to the device. The type here is CIFS_IOCTL_MAGIC, which is defined in cifs_ioctl.h
as 0xCF (and can also be found in the Documentation/ioctl/ioctl-number.txt
:
Presumably, CF is abbreviated CIFS - neat :)
The 'nr' in the macro is a 'request' argument as described in the ioctl manual page (in this case it's 3).
In the fs/cifs/ioctl.c we can see, that when this ioctl command is issued, the 'cifs_ioctl_copychunk' function is called.
This in turn calls 'cifs_file_copychunk_range' from 'cifsfs.c'
Let's try a simple program:
#include <stdio.h> #include <sys/ioctl.h> #include <fcntl.h> #include <errno.h> #include <string.h> #define CIFS_IOCTL_MAGIC 0xCF #define CIFS_IOC_COPYCHUNK_FILE _IOW(CIFS_IOCTL_MAGIC, 3, int) int main( int argc, char *argv[] ) { if( argc == 3 ) { printf("Starting copying...\n", argv[1]); } else if( argc > 3 ) { printf("Too many arguments supplied.\n"); return 1; } else { printf("Two arguments expected - src and dest.\n"); return 1; } char *src_file = argv[1]; int src_fd = open(src_file, O_RDONLY); char *dst_file = argv[2]; int dst_fd = open(dst_file, O_CREAT | O_WRONLY, 0644); int ret = ioctl(dst_fd, CIFS_IOC_COPYCHUNK_FILE, src_fd); ret = errno; if (ret != 0){ fprintf(stderr, "Error %i: %s\n", ret, strerror( ret )); } return ret; }
After compiling it with a name 'copy', I've mounted two smb shares on /tmp/test1 and /tmp/test2.
Let's create a test file and check our new program:
$ dd if=/dev/urandom of=/tmp/test1/file-1mb bs=1M count=1 1+0 records in 1+0 records out 1048576 bytes (1.0 MB, 1.0 MiB) copied, 0.52753 s, 2.0 MB/s $ ./copy /tmp/test1/file-1mb /tmp/test2/file-1mb Starting copying... $ ls -alh /tmp/test2 -rwxr-xr-x 1 user 1666 1.0M May 4 18:15 file-1mb
Looks like it's working. Next step is to copy a larger file and monitor the network usage:
$ dd if=/dev/urandom of=/tmp/test1/file-10gb bs=1M count=10000 ./copy /tmp/test1/file-10gb /tmp/test2/file-10gb Starting copying... Error 22: Invalid argument
Hmm, something is not right. But upon checking the /tmp/test2 directory, we can see that the file is actually there. Just not the whole file:
Using instructions from samba wiki (https://wiki.samba.org/index.php/LinuxCIFS_troubleshooting) I've enabled the cifs debug to inspect this issue:
# echo 'module cifs +p' > /sys/kernel/debug/dynamic_debug/control # echo 'file fs/cifs/* +p' > /sys/kernel/debug/dynamic_debug/control # echo 7 > /proc/fs/cifs/cifsFYI
And I've found this in the dmesg:
CIFS: Status code returned 0xc000000d STATUS_INVALID_PARAMETER CIFS: fs/cifs/smb2maperror.c: Mapping SMB2 status code 0xc000000d to POSIX err -22 CIFS: fs/cifs/smb2ops.c: MaxChunks 256 BytesChunk 1048576 MaxCopy 16777216 CIFS: fs/cifs/ioctl.c: VFS: leaving cifs_ioctl (xid = 881) rc = -22
Further investigating the 'cifs_file_copychunk_range' which is defined in 'cifsfs.c', we see:
|
ssize_t cifs_file_copychunk_range(unsigned int xid,
|
|
struct file *src_file, loff_t off,
|
|
struct file *dst_file, loff_t destoff,
|
|
size_t len, unsigned int flags)
|
|
rc = target_tcon->ses->server->ops->copychunk_range(xid,
|
|
smb_file_src, smb_file_target, off, len, destoff);
|
Checking the structures 'ses', 'server' and 'ops' we find, that the last mentioned is defined in 'smb2ops.c' for mutliple smb versions. Fortunately, all those versions use the same function for 'copychunk_range' method, for example:
smb2_copychunk_range is defined in the same file and uses offsets just like 'cifs_file_copychunk_range'. But going back to the 'cifs_file_copychunk_range' function, we see that it is called with the value '0' for both offsets. However, smb2_copychung_range iterates over file regions of size 'len' (which is the size of part of the file not copied yet) or 'tcon->max_bytes_chunk' (declared by the server) - whichever is the smallest:
smb2_copychunk_range(const unsigned int xid, struct cifsFileInfo *srcfile, struct cifsFileInfo *trgtfile, u64 src_off, u64 len, u64 dest_off) { (...) while (len > 0) { pcchunk->SourceOffset = cpu_to_le64(src_off); pcchunk->TargetOffset = cpu_to_le64(dest_off); pcchunk->Length = cpu_to_le32(min_t(u32, len, tcon->max_bytes_chunk)); /* Request server copy to target from src identified by key */ kfree(retbuf); retbuf = NULL; rc = SMB2_ioctl(xid, tcon, trgtfile->fid.persistent_fid, trgtfile->fid.volatile_fid, FSCTL_SRV_COPYCHUNK_WRITE, (char *)pcchunk, sizeof(struct copychunk_ioctl), CIFSMaxBufSize, (char **)&retbuf, &ret_data_len);
So what's the problem?
After sniffing around with wireshark, I've noticed that the host sends requests to the server, telling it to copy files chunk at the given offset and of the given length. But at some point, the requested transfer length is 0, which causes the SMB server to respond with an -EINVAL error and closes the transmission.
SMB2 (Server Message Block Protocol version 2) SMB2 Header Ioctl Request (0x0b) StructureSize: 0x0039 Reserved: 0000 Function: FSCTL_SRV_COPYCHUNK_WRITE (0x001480f2) GUID handle File: file-5gb Max Ioctl In Size: 0 Max Ioctl Out Size: 16384 Flags: 0x00000001 Reserved: 00000000 Blob Offset: 0x00000000 Blob Length: 0 Out Data: NO DATA Blob Offset: 0x00000078 Blob Length: 56 In Data ResumeKey: Opaque Data Chunk Count: 1 Reserved: 00000000 Chunk Source Offset: 947912704 Target Offset: 947912704 Transfer Length: 0 Reserved: 00000000
After recompiling the CIFS module several times with various debug messages, I've finally found the culprit. It was the above-mentioned 'min_t' function, which casts the u64 value of 'len' into u32, transforming it into 0 when you didn't expect it.
After changing the line to:
it was finally functioning as expected and I could go on with my life (after submitting the patch to the cifs community of course :) )