Skip to content

Commit 5a57248

Browse files
serhiy-storchakajo-hetakluyver
authored
gh-81793: Always call linkat() from os.link(), if available (GH-132517)
This fixes os.link() on platforms (like Linux and OpenIndiana) where the system 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 linkat() function is not available at runtime. * On other platforms, it now raises error if follow_symlinks is passed with a value that does not match the system link() function behavior if if the behavior is not known. Co-authored-by: Joachim Henke <[email protected]> Co-authored-by: Thomas Kluyver <[email protected]>
1 parent e9253eb commit 5a57248

File tree

6 files changed

+99
-66
lines changed

6 files changed

+99
-66
lines changed

Doc/library/os.rst

+1
Original file line numberDiff line numberDiff line change
@@ -2338,6 +2338,7 @@ features:
23382338
This function can support specifying *src_dir_fd* and/or *dst_dir_fd* to
23392339
supply :ref:`paths relative to directory descriptors <dir_fd>`, and :ref:`not
23402340
following symlinks <follow_symlinks>`.
2341+
The default value of *follow_symlinks* is ``False`` on Windows.
23412342

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

Lib/test/test_inspect/test_inspect.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5844,7 +5844,7 @@ def test_operator_module_has_signatures(self):
58445844
self._test_module_has_signatures(operator)
58455845

58465846
def test_os_module_has_signatures(self):
5847-
unsupported_signature = {'chmod', 'utime'}
5847+
unsupported_signature = {'chmod', 'link', 'utime'}
58485848
unsupported_signature |= {name for name in
58495849
['get_terminal_size', 'posix_spawn', 'posix_spawnp',
58505850
'register_at_fork', 'startfile']

Lib/test/test_posix.py

+44
Original file line numberDiff line numberDiff line change
@@ -1521,6 +1521,50 @@ def test_pidfd_open(self):
15211521
self.assertEqual(cm.exception.errno, errno.EINVAL)
15221522
os.close(os.pidfd_open(os.getpid(), 0))
15231523

1524+
@unittest.skipUnless(hasattr(os, "link"), "test needs os.link()")
1525+
def test_link_follow_symlinks(self):
1526+
default_follow = sys.platform.startswith(
1527+
('darwin', 'freebsd', 'netbsd', 'openbsd', 'dragonfly', 'sunos5'))
1528+
default_no_follow = sys.platform.startswith(('win32', 'linux'))
1529+
orig = os_helper.TESTFN
1530+
symlink = orig + 'symlink'
1531+
posix.symlink(orig, symlink)
1532+
self.addCleanup(os_helper.unlink, symlink)
1533+
1534+
with self.subTest('no follow_symlinks'):
1535+
# no follow_symlinks -> platform depending
1536+
link = orig + 'link'
1537+
posix.link(symlink, link)
1538+
self.addCleanup(os_helper.unlink, link)
1539+
if os.link in os.supports_follow_symlinks or default_follow:
1540+
self.assertEqual(posix.lstat(link), posix.lstat(orig))
1541+
elif default_no_follow:
1542+
self.assertEqual(posix.lstat(link), posix.lstat(symlink))
1543+
1544+
with self.subTest('follow_symlinks=False'):
1545+
# follow_symlinks=False -> duplicate the symlink itself
1546+
link = orig + 'link_nofollow'
1547+
try:
1548+
posix.link(symlink, link, follow_symlinks=False)
1549+
except NotImplementedError:
1550+
if os.link in os.supports_follow_symlinks or default_no_follow:
1551+
raise
1552+
else:
1553+
self.addCleanup(os_helper.unlink, link)
1554+
self.assertEqual(posix.lstat(link), posix.lstat(symlink))
1555+
1556+
with self.subTest('follow_symlinks=True'):
1557+
# follow_symlinks=True -> duplicate the target file
1558+
link = orig + 'link_following'
1559+
try:
1560+
posix.link(symlink, link, follow_symlinks=True)
1561+
except NotImplementedError:
1562+
if os.link in os.supports_follow_symlinks or default_follow:
1563+
raise
1564+
else:
1565+
self.addCleanup(os_helper.unlink, link)
1566+
self.assertEqual(posix.lstat(link), posix.lstat(orig))
1567+
15241568

15251569
# tests for the posix *at functions follow
15261570
class TestPosixDirFd(unittest.TestCase):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Fix :func:`os.link` on platforms (like Linux) where the
2+
system :c:func:`!link` function does not follow symlinks. On Linux,
3+
it now follows symlinks by default or if
4+
``follow_symlinks=True`` is specified. On Windows, it now raises an error if
5+
``follow_symlinks=True`` is passed. On macOS, it now raises an error if
6+
``follow_symlinks=False`` is passed and the system :c:func:`!linkat`
7+
function is not available at runtime.

Modules/clinic/posixmodule.c.h

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/posixmodule.c

+43-62
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,11 @@ extern char *ctermid_r(char *);
573573
# define HAVE_FACCESSAT_RUNTIME 1
574574
# define HAVE_FCHMODAT_RUNTIME 1
575575
# define HAVE_FCHOWNAT_RUNTIME 1
576+
#ifdef __wasi__
577+
# define HAVE_LINKAT_RUNTIME 0
578+
# else
576579
# define HAVE_LINKAT_RUNTIME 1
580+
# endif
577581
# define HAVE_FDOPENDIR_RUNTIME 1
578582
# define HAVE_MKDIRAT_RUNTIME 1
579583
# define HAVE_RENAMEAT_RUNTIME 1
@@ -4346,7 +4350,7 @@ os.link
43464350
*
43474351
src_dir_fd : dir_fd = None
43484352
dst_dir_fd : dir_fd = None
4349-
follow_symlinks: bool = True
4353+
follow_symlinks: bool(c_default="-1", py_default="(os.name != 'nt')") = PLACEHOLDER
43504354
43514355
Create a hard link to a file.
43524356
@@ -4364,31 +4368,46 @@ src_dir_fd, dst_dir_fd, and follow_symlinks may not be implemented on your
43644368
static PyObject *
43654369
os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd,
43664370
int dst_dir_fd, int follow_symlinks)
4367-
/*[clinic end generated code: output=7f00f6007fd5269a input=b0095ebbcbaa7e04]*/
4371+
/*[clinic end generated code: output=7f00f6007fd5269a input=1d5e602d115fed7b]*/
43684372
{
43694373
#ifdef MS_WINDOWS
43704374
BOOL result = FALSE;
43714375
#else
43724376
int result;
43734377
#endif
4374-
#if defined(HAVE_LINKAT)
4375-
int linkat_unavailable = 0;
4376-
#endif
43774378

4378-
#ifndef HAVE_LINKAT
4379-
if ((src_dir_fd != DEFAULT_DIR_FD) || (dst_dir_fd != DEFAULT_DIR_FD)) {
4380-
argument_unavailable_error("link", "src_dir_fd and dst_dir_fd");
4381-
return NULL;
4379+
#ifdef HAVE_LINKAT
4380+
if (HAVE_LINKAT_RUNTIME) {
4381+
if (follow_symlinks < 0) {
4382+
follow_symlinks = 1;
4383+
}
43824384
}
4385+
else
43834386
#endif
4384-
4385-
#ifndef MS_WINDOWS
4386-
if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) {
4387-
PyErr_SetString(PyExc_NotImplementedError,
4388-
"link: src and dst must be the same type");
4389-
return NULL;
4390-
}
4387+
{
4388+
if ((src_dir_fd != DEFAULT_DIR_FD) || (dst_dir_fd != DEFAULT_DIR_FD)) {
4389+
argument_unavailable_error("link", "src_dir_fd and dst_dir_fd");
4390+
return NULL;
4391+
}
4392+
/* See issue 85527: link() on Linux works like linkat without AT_SYMLINK_FOLLOW,
4393+
but on Mac it works like linkat *with* AT_SYMLINK_FOLLOW. */
4394+
#if defined(MS_WINDOWS) || defined(__linux__)
4395+
if (follow_symlinks == 1) {
4396+
argument_unavailable_error("link", "follow_symlinks=True");
4397+
return NULL;
4398+
}
4399+
#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__) || (defined(__sun) && defined(__SVR4))
4400+
if (follow_symlinks == 0) {
4401+
argument_unavailable_error("link", "follow_symlinks=False");
4402+
return NULL;
4403+
}
4404+
#else
4405+
if (follow_symlinks >= 0) {
4406+
argument_unavailable_error("link", "follow_symlinks");
4407+
return NULL;
4408+
}
43914409
#endif
4410+
}
43924411

43934412
if (PySys_Audit("os.link", "OOii", src->object, dst->object,
43944413
src_dir_fd == DEFAULT_DIR_FD ? -1 : src_dir_fd,
@@ -4406,44 +4425,18 @@ os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd,
44064425
#else
44074426
Py_BEGIN_ALLOW_THREADS
44084427
#ifdef HAVE_LINKAT
4409-
if ((src_dir_fd != DEFAULT_DIR_FD) ||
4410-
(dst_dir_fd != DEFAULT_DIR_FD) ||
4411-
(!follow_symlinks)) {
4412-
4413-
if (HAVE_LINKAT_RUNTIME) {
4414-
4415-
result = linkat(src_dir_fd, src->narrow,
4416-
dst_dir_fd, dst->narrow,
4417-
follow_symlinks ? AT_SYMLINK_FOLLOW : 0);
4418-
4419-
}
4420-
#ifdef __APPLE__
4421-
else {
4422-
if (src_dir_fd == DEFAULT_DIR_FD && dst_dir_fd == DEFAULT_DIR_FD) {
4423-
/* See issue 41355: This matches the behaviour of !HAVE_LINKAT */
4424-
result = link(src->narrow, dst->narrow);
4425-
} else {
4426-
linkat_unavailable = 1;
4427-
}
4428-
}
4429-
#endif
4428+
if (HAVE_LINKAT_RUNTIME) {
4429+
result = linkat(src_dir_fd, src->narrow,
4430+
dst_dir_fd, dst->narrow,
4431+
follow_symlinks ? AT_SYMLINK_FOLLOW : 0);
44304432
}
44314433
else
4432-
#endif /* HAVE_LINKAT */
4434+
#endif
4435+
{
4436+
/* linkat not available */
44334437
result = link(src->narrow, dst->narrow);
4434-
Py_END_ALLOW_THREADS
4435-
4436-
#ifdef HAVE_LINKAT
4437-
if (linkat_unavailable) {
4438-
/* Either or both dir_fd arguments were specified */
4439-
if (src_dir_fd != DEFAULT_DIR_FD) {
4440-
argument_unavailable_error("link", "src_dir_fd");
4441-
} else {
4442-
argument_unavailable_error("link", "dst_dir_fd");
4443-
}
4444-
return NULL;
44454438
}
4446-
#endif
4439+
Py_END_ALLOW_THREADS
44474440

44484441
if (result)
44494442
return path_error2(src, dst);
@@ -5935,12 +5928,6 @@ internal_rename(path_t *src, path_t *dst, int src_dir_fd, int dst_dir_fd, int is
59355928
return path_error2(src, dst);
59365929

59375930
#else
5938-
if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) {
5939-
PyErr_Format(PyExc_ValueError,
5940-
"%s: src and dst must be the same type", function_name);
5941-
return NULL;
5942-
}
5943-
59445931
Py_BEGIN_ALLOW_THREADS
59455932
#ifdef HAVE_RENAMEAT
59465933
if (dir_fd_specified) {
@@ -10613,12 +10600,6 @@ os_symlink_impl(PyObject *module, path_t *src, path_t *dst,
1061310600

1061410601
#else
1061510602

10616-
if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) {
10617-
PyErr_SetString(PyExc_ValueError,
10618-
"symlink: src and dst must be the same type");
10619-
return NULL;
10620-
}
10621-
1062210603
Py_BEGIN_ALLOW_THREADS
1062310604
#ifdef HAVE_SYMLINKAT
1062410605
if (dir_fd != DEFAULT_DIR_FD) {

0 commit comments

Comments
 (0)