Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fs: bring back macos-specific copyfile(3) #2578

Closed
wants to merge 8 commits into from

Conversation

Artoria2e5
Copy link
Contributor

@Artoria2e5 Artoria2e5 commented Dec 16, 2019

Go check out #3654 instead. @mischnic is awesome.


copyfile(3) does not always do its own open according to permission rules. We can solve that by guarding it with one of our own opens. This way we can keep the cool clonefile thing for huge node_modules and still have sensible results.

MacOS does not have faccess(2) in case you are wondering why I did this. There is only access(2) with the "real UID" shenanigans that will bite us in the future.

This PR partially reverts #2233. Tests in the said PR are kept, and they should pass.

@Artoria2e5
Copy link
Contributor Author

Artoria2e5 commented Dec 16, 2019

We need to do an open first per nodejs/node#26936. Amending.

For the record, this one is possibly due to this part in copyfile: if(chmod(s->dst, (s->sb.st_mode | S_IWUSR) & ~S_IFMT) == 0). Absolute unit.

For clonefileat it should fail correctly, since COPYFILE_UNLINK is not default and the syscall always fails when dest exists

Copy link
Member

@bnoordhuis bnoordhuis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cjihrig You probably want to take a look at this?

src/unix/fs.c Outdated Show resolved Hide resolved
Copy link
Contributor

@cjihrig cjihrig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MacOS does not have faccess(2) in case you are wondering why I did this. There is only access(2) with the "real UID" shenanigans that will bite us in the future.

Is using fstat() an option? At least we wouldn't be creating and unlinking the file.

src/unix/fs.c Outdated
int result;
int err;
size_t bytes_to_send;
int64_t in_offset;
ssize_t bytes_written;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add this line back.

src/unix/fs.c Show resolved Hide resolved
src/unix/fs.c Outdated Show resolved Hide resolved
src/unix/fs.c Outdated Show resolved Hide resolved
@Artoria2e5
Copy link
Contributor Author

Artoria2e5 commented Dec 17, 2019

fstat is not the best idea because ACL is a thing on macOS. The correct way to do so is via the access(2) family of APIs, but macOS does not have a faccess(2) / faccessat(2) that does EUID instead of the UID with AT_EACCESS. A quick Google search I did yesterday finds CarbonStdCLib.o, apparently a C library polyfill, doing something similar for faccess.

Unlinking is what the OS X cp(1) command does for -c anyways, so it kind of is idiomatic in this use.

Gonna fix the branch thing later today (UTC+8).

@Artoria2e5
Copy link
Contributor Author

Artoria2e5 commented Dec 17, 2019

Hmm, looks like there is faccessat(2) on macOS >= 10.10. What's the OS support policy for libuv and nodejs? (Looks like I can do that for node, which only supports macOS >= 10.11.)

@cjihrig
Copy link
Contributor

cjihrig commented Dec 17, 2019

libuv supports macOS >= 10.7. See the list of supported platforms here.

@Artoria2e5
Copy link
Contributor Author

Artoria2e5 commented Dec 17, 2019

A good-ish polyfill for our desired functionality is found in gnulib euidaccess(3) (GPL!!). It gives two implementations: the correct but non-reentrant one using a setre{u,g}id-access-setre{u,g}id pattern and the traditional stat thing which is incorrect with ACL. So which one is more evil here, changing a global state or being incorrect in a case not yet tested?

Since faccessat(2) is a syscall, I can possibly get away with doing a version check like the clone part and syscall(2) the whole thing away on 10.10+. I still need to pick a way to polyfill this feature for [10.7, 10.10), so please do give your suggestion on that. It should look like:

#if defined(__APPLE__) && !TARGET_OS_IPHONE
/** @returns 1: writeable. 2: does not exist. */
static int fs__mac_canwrite(const char* path, int darwin_kern_maj) {
  int res = 0;
  // 10.7+
  if (darwin_kern_maj >= 11) { 
    // TODO: rewrite to use syscall(2) and sys/syscall.h!
    res = faccessat(AT_FDCWD, path, W_OK | R_OK, AT_EACCESS) == 0;
    goto test_exist;
  }
  /* polyfill here */
test_exist: // should be reused by stat or access polyfills
  if (res == 0 && errorno == ENOENT)
    return 2;
  return res;
}

static bool fs__mac_can_copy(const char* path, int darwin_kern_maj, int excl) {
  int res;
  res = fs__mac_canwrite(path, darwin_kern_maj);
  if (res != 2)
    return ! excl;
  // Apple manpage says their dirname does not modify the input. Trust it for now.
  return !! fs__mac_canwrite(dirname(path), darwin_kern_maj);
}
#endif

@richardlau
Copy link
Contributor

I presume the gnulib implementation will be GPL and therefore cannot be used in libuv.

@Artoria2e5
Copy link
Contributor Author

Artoria2e5 commented Dec 17, 2019

Yeah, someone will need to wipe their brain with only the phrase "implement an access check based on stat() or setuid and access(2)" left if they decided to implement it here. Should possibly add a click warning on the gnulib link.

* remove a stat dependency retained from copying the non-Darwin open()
  codepath. We don't need to make it super right here since it's deleted
  soon afterwards, so just a 0700 will do.
* retain whatever copyfile tries to say in the errno, instead of the
  generic error -1.
@stale
Copy link

stale bot commented Jan 13, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale
Copy link

stale bot commented Jul 7, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@cjihrig
Copy link
Contributor

cjihrig commented Aug 9, 2020

With this change, the macOS implementation has more overlap with the Unix implementation than it previously did. The overlap is more prone to getting out of sync - for example, the Unix implementation no longer opens the destination with O_TRUNC like this PR does.

Maybe we should look into using fcopyfile() only for clone operations and use the common implementation for everything else. It would essentially be a macOS specific version of this code block.

@zkochan
Copy link

zkochan commented Jan 9, 2022

ping

@stale stale bot removed the stale label Jan 9, 2022
@Jarred-Sumner
Copy link

To give you a sense of the impact here — bun’s implementation of fs.copyFileSync which uses copyfile() on macOS performs 2x faster than node’s fs.copyFileSync when copying a 1 GB file.

59392F5C-1225-4D3E-9A9E-7D8E1FFDF571

@bnoordhuis
Copy link
Member

I opened #3406 earlier this week to raise the baseline to macOS 10.15. When accepted and merged, I think that would unblock this PR.

@uvesten
Copy link

uvesten commented Jan 14, 2022

To give you a sense of the impact here — bun’s implementation of fs.copyFileSync which uses copyfile() on macOS performs 2x faster than node’s fs.copyFileSync when copying a 1 GB file.

That's good, but I thought the main issue here was that APFS Copy on Write is not supported. When it is supported, the speedup should be closer to infinity than 2x.

@Jarred-Sumner
Copy link

Jarred-Sumner commented Jan 14, 2022 via email

@bnoordhuis
Copy link
Member

The baseline is now macos 10.15. There's one remaining comment from @cjihrig so I defer to him on whether it's ready for merging. Let me know if I should also review.

Copy link
Contributor

@cjihrig cjihrig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM if the tests pass.


/* Check OS version. Cloning is only supported on macOS >= 10.12. */
if (req->flags & UV_FS_COPYFILE_FICLONE_FORCE) {
if (can_clone == 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (can_clone == 0) {
if (0 == uv__load_relaxed(&can_clone)) {

if (1 != sscanf(buf, "%d", &major))
abort();

can_clone = -1 + 2 * (major >= 16); /* macOS >= 10.12 */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
can_clone = -1 + 2 * (major >= 16); /* macOS >= 10.12 */
/* macOS >= 10.12 */
uv__store_relaxed(&can_clone, -1 + 2 * (major >= 16));

can_clone = -1 + 2 * (major >= 16); /* macOS >= 10.12 */
}

if (can_clone < 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (can_clone < 0)
if (0 > uv__load_relaxed(&can_clone))

Comment on lines +1163 to +1173
/* Copyfile(2) tries to chmod the file when a rw open fails. This causes
* nodejs/node#26936.
* Make it behave by doing our own open-test, because faccessat(2) is not
* present on OS X < 10.10. */
dstfd = uv_fs_open(NULL,
&fs_req,
req->new_path,
dst_flags,
S_IRWXU,
NULL);
uv_fs_req_cleanup(&fs_req);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With 10.15 as the baseline, it's presumably okay to use faccessat() now?

Comment on lines +1175 to +1190
if (dstfd < 0) {
return dstfd;
} else {
err = uv__close_nocheckstdio(dstfd);
if (err) {
return err;
}
/* We might have created it. We also don't want clone to fail without
* UV_FS_COPYFILE_EXCL. */
unlink(req->new_path);
}

if (copyfile(req->path, req->new_path, NULL, flags)) {
return UV__ERR(errno);
}
return 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stylistic:

Suggested change
if (dstfd < 0) {
return dstfd;
} else {
err = uv__close_nocheckstdio(dstfd);
if (err) {
return err;
}
/* We might have created it. We also don't want clone to fail without
* UV_FS_COPYFILE_EXCL. */
unlink(req->new_path);
}
if (copyfile(req->path, req->new_path, NULL, flags)) {
return UV__ERR(errno);
}
return 0;
if (dstfd < 0)
return dstfd;
err = uv__close_nocheckstdio(dstfd);
if (err)
return err;
/* We might have created it. We also don't want clone to fail without
* UV_FS_COPYFILE_EXCL. */
unlink(req->new_path);
if (copyfile(req->path, req->new_path, NULL, flags))
return UV__ERR(errno);
return 0;

return UV__ERR(errno);
}
return 0;
#else
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#else
#else /* defined(__APPLE__) && !TARGET_OS_IPHONE */

@@ -1287,6 +1359,7 @@ static ssize_t uv__fs_copyfile(uv_fs_t* req) {

errno = UV__ERR(result);
return -1;
#endif
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#endif
#endif /* defined(__APPLE__) && !TARGET_OS_IPHONE */

Makes it easier to keep track of what else/endif belongs to what ifdef.

@stale
Copy link

stale bot commented Apr 16, 2022

This issue has been automatically marked as stale because it has not had recent activity. Thank you for your contributions.

@stale stale bot added the stale label Apr 16, 2022
@zkochan
Copy link

zkochan commented Apr 16, 2022

Never Gonna Give You Up

@stale stale bot removed the stale label Apr 16, 2022
@FezVrasta
Copy link

Adding my comment so my anti stale bot will take care of this pr from now on

@bnoordhuis
Copy link
Member

There's some unaddressed feedback I left in February. Most is stylistic but the faccessat() question needs an answer before this can go in.

Also, a gentle reminder that bots and their owners are likely to meet with swift and permanent bans.

@stale
Copy link

stale bot commented Jun 12, 2022

This issue has been automatically marked as stale because it has not had recent activity. Thank you for your contributions.

@stale stale bot added the stale label Jun 12, 2022
@zkochan
Copy link

zkochan commented Jun 12, 2022

Where is the anti stale bot?

@stale stale bot removed the stale label Jun 12, 2022
@cjihrig cjihrig added the not-stale Issues that should never be marked stale label Jun 12, 2022
@cjihrig
Copy link
Contributor

cjihrig commented Jun 12, 2022

I've added the not-stale label. Hopefully that will keep stale bot away.

@bnoordhuis
Copy link
Member

@Artoria2e5 Do you plan on picking up this pull request again? If not, maybe someone else wants to step up and get this over the finish line?

@mischnic
Copy link
Contributor

If not, maybe someone else wants to step up and get this over the finish line?

I've tried to incorporate your feedback: #3654

@Artoria2e5
Copy link
Contributor Author

In light of an updated PR being available I am closing this one. Just don't have time for code when I am all stuck at McD, I am afraid.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
not-stale Issues that should never be marked stale
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet