Skip to content

gh-81793: always call linkat() from os.link(), if available #24997

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

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Lib/test/test_posix.py
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,27 @@ def test_pidfd_open(self):
self.assertEqual(cm.exception.errno, errno.EINVAL)
os.close(os.pidfd_open(os.getpid(), 0))

@unittest.skipUnless(
hasattr(os, "link") and os.link in os.supports_follow_symlinks,
"test needs follow_symlinks support in os.link()"
)
def test_link_follow_symlinks(self):
symlink_fn = os_helper.TESTFN + 'symlink'
link_following = os_helper.TESTFN + 'link_following'
link_nofollow = os_helper.TESTFN + 'link_nofollow'
posix.symlink(os_helper.TESTFN, symlink_fn)
self.addCleanup(os_helper.unlink, symlink_fn)

# follow_symlinks=False -> duplicate the symlink itself
posix.link(symlink_fn, link_nofollow, follow_symlinks=False)
self.addCleanup(os_helper.unlink, link_nofollow)
self.assertEqual(posix.lstat(link_nofollow), posix.lstat(symlink_fn))

# follow_symlinks=True -> duplicate the target file
posix.link(symlink_fn, link_following, follow_symlinks=True)
self.addCleanup(os_helper.unlink, link_following)
self.assertEqual(posix.lstat(link_following), posix.lstat(os_helper.TESTFN))


# tests for the posix *at functions follow
class TestPosixDirFd(unittest.TestCase):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fix os.link() on platforms (like Linux) where the system link() function does not follow symlinks
50 changes: 30 additions & 20 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -4383,31 +4383,41 @@ os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd,
#else
Py_BEGIN_ALLOW_THREADS
#ifdef HAVE_LINKAT
if ((src_dir_fd != DEFAULT_DIR_FD) ||
(dst_dir_fd != DEFAULT_DIR_FD) ||
(!follow_symlinks)) {

if (HAVE_LINKAT_RUNTIME) {

result = linkat(src_dir_fd, src->narrow,
dst_dir_fd, dst->narrow,
follow_symlinks ? AT_SYMLINK_FOLLOW : 0);

}
if (HAVE_LINKAT_RUNTIME) {
result = linkat(src_dir_fd, src->narrow,
dst_dir_fd, dst->narrow,
follow_symlinks ? AT_SYMLINK_FOLLOW : 0);
}
#ifdef __APPLE__
else {
if (src_dir_fd == DEFAULT_DIR_FD && dst_dir_fd == DEFAULT_DIR_FD) {
/* See issue 41355: This matches the behaviour of !HAVE_LINKAT */
result = link(src->narrow, dst->narrow);
} else {
linkat_unavailable = 1;
}
else {
if (src_dir_fd == DEFAULT_DIR_FD && dst_dir_fd == DEFAULT_DIR_FD && follow_symlinks) {
/* See issue 41355: This matches the behaviour of !HAVE_LINKAT */
result = link(src->narrow, dst->narrow);
} else {
linkat_unavailable = 1;
}
}
#endif

#else /* linkat not available */
/* See issue 41355: link() on Linux works like linkat without AT_SYMLINK_FOLLOW,
but on Mac it works like linkat *with* AT_SYMLINK_FOLLOW. */
#ifdef __APPLE__
if (!follow_symlinks) {
PyErr_SetString(PyExc_NotImplementedError,
"link: follow_symlinks=False unavailable on this platform");
return NULL;
} else {
#else
if (follow_symlinks) {
PyErr_SetString(PyExc_NotImplementedError,
"link: follow_symlinks=True unavailable on this platform");
Comment on lines +4412 to +4414
Copy link
Member

Choose a reason for hiding this comment

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

follow_symlinks is true by default, so os.link() will always fail if linkat is not available.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that's desirable? follow_symlinks is documented as true by default, so if that's unlikely to work, it seems better to error out than to ignore it.

linkat has been in Linux since 2006 and FreeBSD since 2009, so hopefully this isn't a case that many people will run into.

Copy link
Member

Choose a reason for hiding this comment

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

No, it is not desirable. Otherwise we would just not implement os.link() without linkat.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, you could still pass follow_symlinks=False if you're happy with that behaviour, so it's more useful than the function not existing at all. What alternative are you suggesting?

  • Should os.link() implement a symlink-following fallback with 2 system calls? Is there a race condition there?
  • Should the follow_symlinks default be documented as platform dependent? That would be accurate but surprising, since most functions with that parameter default to True, and it makes it easy to write code that does something slightly different on Mac & Linux.

But is there any relevant platform or configuration where linkat() is not available? It looks like this has been ubiquitous for well over a decade, so maybe the fallback is kind of academic anyway.

Copy link
Member

Choose a reason for hiding this comment

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

I am working on this issue now.

Copy link
Member

Choose a reason for hiding this comment

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

See #81793.

return NULL;
} else {
#endif /* __APPLE__ */
result = link(src->narrow, dst->narrow);
}
else
#endif /* HAVE_LINKAT */
result = link(src->narrow, dst->narrow);
Py_END_ALLOW_THREADS

#ifdef HAVE_LINKAT
Expand Down
Loading