copyfile can't copy symlinks without following from network mounted drives

Originator:zorgiepoo
Number:rdar://23059163 Date Originated:09-Oct-2015 11:39 PM
Status:Open Resolved:
Product:OS X Product Version:10.11 (15A284)
Classification: Reproducible:Always
 
Summary:
The copyfile() cannot copy symbolic links without following them from a network mount. It errors with "Invalid argument"

I have been testing this on my MacBook Air and iMac (both running 10.11.0 (15A284). However I have also been able to reproduce this on my virtual machine on my iMac which is running on the latest version of 10.8, so this bug has been around for a while.

Note this also affects NSFileManager, and its -copyItemAtURL:toURL:error: method, since that method too is unable to copy the symbolic link.

Also note that this also affects directories that contain symbolic links (like applications for example). The copy is in progress until a symlink is encountered.

As a regression, though, the deprecated FSCopyObjectSync() function does work. After poking a bit with ditto's disassembly, I notice they don't use copyfile() much (aside from possibly preserving metadata), so I'm not too surprised this bug has gone unnoticed.

Steps to Reproduce:
1. Download the attached Xcode project and build the utility. This copyfile utility takes two arguments, the source and destination file paths; it copies the file at source to destination.

2. Connect to another Mac via Finder's file sharing. Mount the other Mac's home directory. This is over SMB (or is it SMB2..? anyway..)

3. Create a file and a symbolic link pointing to it on the mounted Mac's home directory. Eg:

cd /Volumes/iMac_user/
echo "foo" > foo.txt
ln -s foo.txt bar.txt

(The destination of the link really doesn't matter for this bug's purposes, but you'll see why I included it for step #5)

4. Try to copy the symbolic link, bar.txt to your own machine using the command line tool from the project I've attached. Eg:

./copyfile /Volumes/iMac_user/bar.txt /Users/MacBookAir_user/Desktop/bar.txt

Notice that the program spits out an error message and fails. I get a message like this:

Failed copying /Volumes/iMac_user/bar.txt to /Users/MacBookAir_user/Desktop/bar.txt with copyfile(3) error: 22 - Invalid argument

5. If you omit COPYFILE_NOFOLLOW in the code and re-build, you will notice that the file can be copied. However this follows symbolic links which is *not* what I want to do. If you try calling -[NSFileManager copyItemAtURL:toURL:error:] you will run into similar issues as well. However using FSCopyObjectSync or the async version works.

Expected Results:
I expect copyfile() to be able to copy symlinks from other network mounted drives. Note that this issue is only prevalent when another network drive is involved. Copying symlinks locally does not cause me any issue.

Actual Results:
copyfile() fails at copying symlinks from network mounted drives. Finder and cp and ditto all work fine however.

Version:
Xcode 7.0.1 (7A1001)
OS X 10.11.0 (15A284)

Notes:


Configuration:
I have reproduced this on my MBA running 10.11.0 (15A284), and my iMac running 10.11.0 (15A284). Additionally I reproduced it on my VM on my iMac running latest version of 10.8.

MacBook Air.spx - configuration file has been successfully uploaded.

Attachments:
'MacBook Air.spx' and 'iMac.spx' were successfully uploaded.

Attached project is here: https://zgcoder.net/zfw/copyfile.zip

Comments

I debugged the copyfile.c source code and found this snippet:

if (islnk) {
    size_t sz = (size_t)s->sb.st_size + 1;
    char *bp;

    bp = calloc(1, sz);
    if (bp == NULL) {
        copyfile_warn("cannot allocate %zd bytes", sz);
        return -1;
    }
    if (readlink(s->src, bp, sz-1) == -1) {
        copyfile_warn("cannot readlink %s", s->src);
        free(bp);
        return -1;
    }
    if (symlink(bp, s->dst) == -1) {
        if (errno != EEXIST || (s->flags & COPYFILE_EXCL)) {
            copyfile_warn("Cannot make symlink %s", s->dst);
            free(bp);
            return -1;
        }
    }

When running a test program against this code, I noticed that the value returned back from s->sb.st_size is 0. That is why symlink() errors on invalid argument, because the buffer is just zero'ed out (there is only 1 character read which is just 0 anyway).

I then came to the conclusion that lstat(2), stat(1), and Finder's get info window report that the file size of symbolic links on my network mounts (which in this case are Mac's running 10.11.0) are zero bytes in size. This only occurs with symlinks on remote (SMB?) drives. So this seems to be the main issue, although copyfile is still affected by this bug.

I'll open another rdar for this shortly.


Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!