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 <37883863+jo-he@users.noreply.github.com> Co-authored-by: Thomas Kluyver <takowl@gmail.com>
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 44 additions & 0 deletions
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):
Lines changed: 7 additions & 0 deletions
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

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/posixmodule.c

Lines changed: 43 additions & 62 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy