Skip to content

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

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

Merged
Merged
1 change: 1 addition & 0 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2338,6 +2338,7 @@ features:
This function can support specifying *src_dir_fd* and/or *dst_dir_fd* to
supply :ref:`paths relative to directory descriptors <dir_fd>`, and :ref:`not
following symlinks <follow_symlinks>`.
The default value of *follow_symlinks* is ``False`` on Windows.

.. audit-event:: os.link src,dst,src_dir_fd,dst_dir_fd os.link

Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -5799,7 +5799,7 @@ def test_operator_module_has_signatures(self):
self._test_module_has_signatures(operator)

def test_os_module_has_signatures(self):
unsupported_signature = {'chmod', 'utime'}
unsupported_signature = {'chmod', 'link', 'utime'}
unsupported_signature |= {name for name in
['get_terminal_size', 'posix_spawn', 'posix_spawnp',
'register_at_fork', 'startfile']
Expand Down
40 changes: 40 additions & 0 deletions Lib/test/test_posix.py
Original file line number Diff line number Diff line change
Expand Up @@ -1521,6 +1521,46 @@ 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"), "test needs os.link()")
def test_link_follow_symlinks(self):
orig = os_helper.TESTFN
symlink = orig + 'symlink'
posix.symlink(orig, symlink)
self.addCleanup(os_helper.unlink, symlink)

link = orig + 'link'
posix.link(symlink, link)
self.addCleanup(os_helper.unlink, link)
default_follow = sys.platform.startswith(
('darwin', 'freebsd', 'netbsd', 'openbsd', 'dragonfly', 'sunos5'))
default_no_follow = sys.platform.startswith(('win32', 'linux'))
if os.link in os.supports_follow_symlinks or default_follow:
self.assertEqual(posix.lstat(link), posix.lstat(orig))
elif default_no_follow:
self.assertEqual(posix.lstat(link), posix.lstat(symlink))

# follow_symlinks=False -> duplicate the symlink itself
link_nofollow = orig + 'link_nofollow'
try:
posix.link(symlink, link_nofollow, follow_symlinks=False)
except NotImplementedError:
if os.link in os.supports_follow_symlinks or default_no_follow:
raise
else:
self.addCleanup(os_helper.unlink, link_nofollow)
self.assertEqual(posix.lstat(link_nofollow), posix.lstat(symlink))

# follow_symlinks=True -> duplicate the target file
link_following = orig + 'link_following'
try:
posix.link(symlink, link_following, follow_symlinks=True)
except NotImplementedError:
if os.link in os.supports_follow_symlinks or default_follow:
raise
else:
self.addCleanup(os_helper.unlink, link_following)
self.assertEqual(posix.lstat(link_following), posix.lstat(orig))


# 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,7 @@
Fix :func:`os.link` on platforms (like Linux) where the
system :c:func:`!link` function does not follow symlinks. On Linux,
it now follows symlinks by default and if
``follow_symlinks=True`` is specified. On Windows, it now raises error if
``follow_symlinks=True`` is passed. On macOS, it now raises error if
``follow_symlinks=False`` is passed and the system :c:func:`!linkat`
function is not available at runtime.
6 changes: 3 additions & 3 deletions Modules/clinic/posixmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 41 additions & 44 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -4323,7 +4323,7 @@ os.link
*
src_dir_fd : dir_fd = None
dst_dir_fd : dir_fd = None
follow_symlinks: bool = True
follow_symlinks: bool(c_default="-1", py_default="(os.name != 'nt')") = PLACEHOLDER

Create a hard link to a file.

Expand All @@ -4341,25 +4341,48 @@ src_dir_fd, dst_dir_fd, and follow_symlinks may not be implemented on your
static PyObject *
os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd,
int dst_dir_fd, int follow_symlinks)
/*[clinic end generated code: output=7f00f6007fd5269a input=b0095ebbcbaa7e04]*/
/*[clinic end generated code: output=7f00f6007fd5269a input=1d5e602d115fed7b]*/
{
#ifdef MS_WINDOWS
BOOL result = FALSE;
#else
int result;
#endif
#if defined(HAVE_LINKAT)
int linkat_unavailable = 0;
#endif

#ifndef HAVE_LINKAT
if ((src_dir_fd != DEFAULT_DIR_FD) || (dst_dir_fd != DEFAULT_DIR_FD)) {
argument_unavailable_error("link", "src_dir_fd and dst_dir_fd");
return NULL;
#ifdef HAVE_LINKAT
if (HAVE_LINKAT_RUNTIME) {
if (follow_symlinks < 0) {
follow_symlinks = 1;
}
}
else
#endif
{
if ((src_dir_fd != DEFAULT_DIR_FD) || (dst_dir_fd != DEFAULT_DIR_FD)) {
argument_unavailable_error("link", "src_dir_fd and dst_dir_fd");
return NULL;
}
/* See issue 41355: link() on Linux works like linkat without AT_SYMLINK_FOLLOW,
but on Mac it works like linkat *with* AT_SYMLINK_FOLLOW. */
#if defined(MS_WINDOWS) || defined(__linux__)
if (follow_symlinks == 1) {
argument_unavailable_error("link", "follow_symlinks=True");
return NULL;
}
#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__) || (defined(__sun) && defined(__SVR4))
if (follow_symlinks == 0) {
argument_unavailable_error("link", "follow_symlinks=False");
return NULL;
}
#else
if (follow_symlinks >= 0) {
argument_unavailable_error("link", "follow_symlinks");
return NULL;
}
#endif
}

#ifndef MS_WINDOWS
#ifdef MS_WINDOWS
if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) {
PyErr_SetString(PyExc_NotImplementedError,
"link: src and dst must be the same type");
Expand All @@ -4383,44 +4406,18 @@ 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);

}
#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;
}
}
#endif
if (HAVE_LINKAT_RUNTIME) {
result = linkat(src_dir_fd, src->narrow,
dst_dir_fd, dst->narrow,
follow_symlinks ? AT_SYMLINK_FOLLOW : 0);
}
else
#endif /* HAVE_LINKAT */
#endif
{
/* linkat not available */
result = link(src->narrow, dst->narrow);
Py_END_ALLOW_THREADS

#ifdef HAVE_LINKAT
if (linkat_unavailable) {
/* Either or both dir_fd arguments were specified */
if (src_dir_fd != DEFAULT_DIR_FD) {
argument_unavailable_error("link", "src_dir_fd");
} else {
argument_unavailable_error("link", "dst_dir_fd");
}
return NULL;
}
#endif
Py_END_ALLOW_THREADS

if (result)
return path_error2(src, dst);
Expand Down
Loading