Skip to content

Commit a8f488a

Browse files
serhiy-storchakajo-hetakluyver
committed
pythongh-81793: Always call linkat() from os.link(), if available
This fixes os.link() on platforms (like Linux and OpenIndiana) where the system link() function does not follow symlinks. * On Linux and OpenIndiana, 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 842ab81 commit a8f488a

File tree

5 files changed

+91
-47
lines changed

5 files changed

+91
-47
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_posix.py

+39
Original file line numberDiff line numberDiff line change
@@ -1521,6 +1521,45 @@ 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+
orig = os_helper.TESTFN
1527+
symlink = orig + 'symlink'
1528+
posix.symlink(orig, symlink)
1529+
self.addCleanup(os_helper.unlink, symlink)
1530+
1531+
link = orig + 'link'
1532+
posix.link(symlink, link)
1533+
self.addCleanup(os_helper.unlink, link)
1534+
default_follow = sys.platform.startswith(('darwin', 'freebsd', 'netbsd', 'openbsd', 'dragonfly'))
1535+
default_no_follow = sys.platform.startswith(('win32', 'linux', 'sunos5'))
1536+
if os.link in os.supports_follow_symlinks or default_follow:
1537+
self.assertEqual(posix.lstat(link), posix.lstat(orig))
1538+
elif default_no_follow:
1539+
self.assertEqual(posix.lstat(link), posix.lstat(symlink))
1540+
1541+
# follow_symlinks=False -> duplicate the symlink itself
1542+
link_nofollow = orig + 'link_nofollow'
1543+
try:
1544+
posix.link(symlink, link_nofollow, follow_symlinks=False)
1545+
except NotImplementedError:
1546+
if os.link in os.supports_follow_symlinks or default_no_follow:
1547+
raise
1548+
else:
1549+
self.addCleanup(os_helper.unlink, link_nofollow)
1550+
self.assertEqual(posix.lstat(link_nofollow), posix.lstat(symlink))
1551+
1552+
# follow_symlinks=True -> duplicate the target file
1553+
link_following = orig + 'link_following'
1554+
try:
1555+
posix.link(symlink, link_following, follow_symlinks=True)
1556+
except NotImplementedError:
1557+
if os.link in os.supports_follow_symlinks or default_follow:
1558+
raise
1559+
else:
1560+
self.addCleanup(os_helper.unlink, link_following)
1561+
self.assertEqual(posix.lstat(link_following), posix.lstat(orig))
1562+
15241563

15251564
# tests for the posix *at functions follow
15261565
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 and OpenIndiana) where the
2+
system :c:finc:`!link` function does not follow symlinks. On Linux and
3+
OpenIndiana, it now follows symlinks by default and if
4+
``follow_symlinks=True`` is specified. On Windows, it now raises error if
5+
``follow_symlinks=True`` is passed. On macOS, it now raises error if
6+
``follow_symlinks=False`` is passed and the system :c:finc:`!linkat`
7+
function is not available at runtime.

Modules/clinic/posixmodule.c.h

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

Modules/posixmodule.c

+42-45
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ extern char *ctermid_r(char *);
427427
/* Something to implement in autoconf, not present in autoconf 2.69 */
428428
# define HAVE_STRUCT_STAT_ST_FSTYPE 1
429429
#endif
430-
430+
#undef HAVE_LINKAT
431431

432432
// --- Apple __builtin_available() macros -----------------------------------
433433

@@ -4323,7 +4323,7 @@ os.link
43234323
*
43244324
src_dir_fd : dir_fd = None
43254325
dst_dir_fd : dir_fd = None
4326-
follow_symlinks: bool = True
4326+
follow_symlinks: bool(c_default="-1", py_default="(os.name != 'nt')") = PLACEHOLDER
43274327
43284328
Create a hard link to a file.
43294329
@@ -4341,25 +4341,48 @@ src_dir_fd, dst_dir_fd, and follow_symlinks may not be implemented on your
43414341
static PyObject *
43424342
os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd,
43434343
int dst_dir_fd, int follow_symlinks)
4344-
/*[clinic end generated code: output=7f00f6007fd5269a input=b0095ebbcbaa7e04]*/
4344+
/*[clinic end generated code: output=7f00f6007fd5269a input=f6a681a558380a15]*/
43454345
{
43464346
#ifdef MS_WINDOWS
43474347
BOOL result = FALSE;
43484348
#else
43494349
int result;
43504350
#endif
4351-
#if defined(HAVE_LINKAT)
4352-
int linkat_unavailable = 0;
4353-
#endif
43544351

4355-
#ifndef HAVE_LINKAT
4356-
if ((src_dir_fd != DEFAULT_DIR_FD) || (dst_dir_fd != DEFAULT_DIR_FD)) {
4357-
argument_unavailable_error("link", "src_dir_fd and dst_dir_fd");
4358-
return NULL;
4352+
#ifdef HAVE_LINKAT
4353+
if (HAVE_LINKAT_RUNTIME) {
4354+
if (follow_symlinks < 0) {
4355+
follow_symlinks = 1;
4356+
}
43594357
}
4358+
else
43604359
#endif
4360+
{
4361+
if ((src_dir_fd != DEFAULT_DIR_FD) || (dst_dir_fd != DEFAULT_DIR_FD)) {
4362+
argument_unavailable_error("link", "src_dir_fd and dst_dir_fd");
4363+
return NULL;
4364+
}
4365+
/* See issue 41355: link() on Linux works like linkat without AT_SYMLINK_FOLLOW,
4366+
but on Mac it works like linkat *with* AT_SYMLINK_FOLLOW. */
4367+
#if defined(MS_WINDOWS) || defined(__linux__) || (defined(__sun) && defined(__SVR4))
4368+
if (follow_symlinks == 1) {
4369+
argument_unavailable_error("link", "follow_symlinks=True");
4370+
return NULL;
4371+
}
4372+
#elif defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) || defined(__DragonFly__)
4373+
if (follow_symlinks == 0) {
4374+
argument_unavailable_error("link", "follow_symlinks=False");
4375+
return NULL;
4376+
}
4377+
#else
4378+
if (follow_symlinks >= 0) {
4379+
argument_unavailable_error("link", "follow_symlinks");
4380+
return NULL;
4381+
}
4382+
#endif
4383+
}
43614384

4362-
#ifndef MS_WINDOWS
4385+
#ifdef MS_WINDOWS
43634386
if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) {
43644387
PyErr_SetString(PyExc_NotImplementedError,
43654388
"link: src and dst must be the same type");
@@ -4383,44 +4406,18 @@ os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd,
43834406
#else
43844407
Py_BEGIN_ALLOW_THREADS
43854408
#ifdef HAVE_LINKAT
4386-
if ((src_dir_fd != DEFAULT_DIR_FD) ||
4387-
(dst_dir_fd != DEFAULT_DIR_FD) ||
4388-
(!follow_symlinks)) {
4389-
4390-
if (HAVE_LINKAT_RUNTIME) {
4391-
4392-
result = linkat(src_dir_fd, src->narrow,
4393-
dst_dir_fd, dst->narrow,
4394-
follow_symlinks ? AT_SYMLINK_FOLLOW : 0);
4395-
4396-
}
4397-
#ifdef __APPLE__
4398-
else {
4399-
if (src_dir_fd == DEFAULT_DIR_FD && dst_dir_fd == DEFAULT_DIR_FD) {
4400-
/* See issue 41355: This matches the behaviour of !HAVE_LINKAT */
4401-
result = link(src->narrow, dst->narrow);
4402-
} else {
4403-
linkat_unavailable = 1;
4404-
}
4405-
}
4406-
#endif
4409+
if (HAVE_LINKAT_RUNTIME) {
4410+
result = linkat(src_dir_fd, src->narrow,
4411+
dst_dir_fd, dst->narrow,
4412+
follow_symlinks ? AT_SYMLINK_FOLLOW : 0);
44074413
}
44084414
else
4409-
#endif /* HAVE_LINKAT */
4415+
#endif
4416+
{
4417+
/* linkat not available */
44104418
result = link(src->narrow, dst->narrow);
4411-
Py_END_ALLOW_THREADS
4412-
4413-
#ifdef HAVE_LINKAT
4414-
if (linkat_unavailable) {
4415-
/* Either or both dir_fd arguments were specified */
4416-
if (src_dir_fd != DEFAULT_DIR_FD) {
4417-
argument_unavailable_error("link", "src_dir_fd");
4418-
} else {
4419-
argument_unavailable_error("link", "dst_dir_fd");
4420-
}
4421-
return NULL;
44224419
}
4423-
#endif
4420+
Py_END_ALLOW_THREADS
44244421

44254422
if (result)
44264423
return path_error2(src, dst);

0 commit comments

Comments
 (0)