mirror of
https://github.com/python/cpython.git
synced 2025-07-07 19:35:27 +00:00
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>
This commit is contained in:
parent
e9253ebf74
commit
5a57248b22
6 changed files with 100 additions and 67 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -5844,7 +5844,7 @@ class TestSignatureDefinitions(unittest.TestCase):
|
|||
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']
|
||||
|
|
|
@ -1521,6 +1521,50 @@ class PosixTester(unittest.TestCase):
|
|||
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):
|
||||
default_follow = sys.platform.startswith(
|
||||
('darwin', 'freebsd', 'netbsd', 'openbsd', 'dragonfly', 'sunos5'))
|
||||
default_no_follow = sys.platform.startswith(('win32', 'linux'))
|
||||
orig = os_helper.TESTFN
|
||||
symlink = orig + 'symlink'
|
||||
posix.symlink(orig, symlink)
|
||||
self.addCleanup(os_helper.unlink, symlink)
|
||||
|
||||
with self.subTest('no follow_symlinks'):
|
||||
# no follow_symlinks -> platform depending
|
||||
link = orig + 'link'
|
||||
posix.link(symlink, link)
|
||||
self.addCleanup(os_helper.unlink, link)
|
||||
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))
|
||||
|
||||
with self.subTest('follow_symlinks=False'):
|
||||
# follow_symlinks=False -> duplicate the symlink itself
|
||||
link = orig + 'link_nofollow'
|
||||
try:
|
||||
posix.link(symlink, link, 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)
|
||||
self.assertEqual(posix.lstat(link), posix.lstat(symlink))
|
||||
|
||||
with self.subTest('follow_symlinks=True'):
|
||||
# follow_symlinks=True -> duplicate the target file
|
||||
link = orig + 'link_following'
|
||||
try:
|
||||
posix.link(symlink, link, follow_symlinks=True)
|
||||
except NotImplementedError:
|
||||
if os.link in os.supports_follow_symlinks or default_follow:
|
||||
raise
|
||||
else:
|
||||
self.addCleanup(os_helper.unlink, link)
|
||||
self.assertEqual(posix.lstat(link), posix.lstat(orig))
|
||||
|
||||
|
||||
# tests for the posix *at functions follow
|
||||
class TestPosixDirFd(unittest.TestCase):
|
||||
|
|
|
@ -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 or if
|
||||
``follow_symlinks=True`` is specified. On Windows, it now raises an error if
|
||||
``follow_symlinks=True`` is passed. On macOS, it now raises an error if
|
||||
``follow_symlinks=False`` is passed and the system :c:func:`!linkat`
|
||||
function is not available at runtime.
|
6
Modules/clinic/posixmodule.c.h
generated
6
Modules/clinic/posixmodule.c.h
generated
|
@ -1471,7 +1471,7 @@ os_getcwdb(PyObject *module, PyObject *Py_UNUSED(ignored))
|
|||
|
||||
PyDoc_STRVAR(os_link__doc__,
|
||||
"link($module, /, src, dst, *, src_dir_fd=None, dst_dir_fd=None,\n"
|
||||
" follow_symlinks=True)\n"
|
||||
" follow_symlinks=(os.name != \'nt\'))\n"
|
||||
"--\n"
|
||||
"\n"
|
||||
"Create a hard link to a file.\n"
|
||||
|
@ -1530,7 +1530,7 @@ os_link(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwn
|
|||
path_t dst = PATH_T_INITIALIZE_P("link", "dst", 0, 0, 0, 0);
|
||||
int src_dir_fd = DEFAULT_DIR_FD;
|
||||
int dst_dir_fd = DEFAULT_DIR_FD;
|
||||
int follow_symlinks = 1;
|
||||
int follow_symlinks = -1;
|
||||
|
||||
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
|
||||
/*minpos*/ 2, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
|
||||
|
@ -13398,4 +13398,4 @@ os__emscripten_debugger(PyObject *module, PyObject *Py_UNUSED(ignored))
|
|||
#ifndef OS__EMSCRIPTEN_DEBUGGER_METHODDEF
|
||||
#define OS__EMSCRIPTEN_DEBUGGER_METHODDEF
|
||||
#endif /* !defined(OS__EMSCRIPTEN_DEBUGGER_METHODDEF) */
|
||||
/*[clinic end generated code: output=a5ca2541f2af5462 input=a9049054013a1b77]*/
|
||||
/*[clinic end generated code: output=f7b5635e0b948be4 input=a9049054013a1b77]*/
|
||||
|
|
|
@ -573,7 +573,11 @@ extern char *ctermid_r(char *);
|
|||
# define HAVE_FACCESSAT_RUNTIME 1
|
||||
# define HAVE_FCHMODAT_RUNTIME 1
|
||||
# define HAVE_FCHOWNAT_RUNTIME 1
|
||||
#ifdef __wasi__
|
||||
# define HAVE_LINKAT_RUNTIME 0
|
||||
# else
|
||||
# define HAVE_LINKAT_RUNTIME 1
|
||||
# endif
|
||||
# define HAVE_FDOPENDIR_RUNTIME 1
|
||||
# define HAVE_MKDIRAT_RUNTIME 1
|
||||
# define HAVE_RENAMEAT_RUNTIME 1
|
||||
|
@ -4346,7 +4350,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.
|
||||
|
||||
|
@ -4364,31 +4368,46 @@ 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 85527: 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
|
||||
if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) {
|
||||
PyErr_SetString(PyExc_NotImplementedError,
|
||||
"link: src and dst must be the same type");
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (PySys_Audit("os.link", "OOii", src->object, dst->object,
|
||||
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,
|
|||
#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 */
|
||||
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
|
||||
{
|
||||
/* linkat not available */
|
||||
result = link(src->narrow, dst->narrow);
|
||||
}
|
||||
Py_END_ALLOW_THREADS
|
||||
|
||||
if (result)
|
||||
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
|
|||
return path_error2(src, dst);
|
||||
|
||||
#else
|
||||
if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"%s: src and dst must be the same type", function_name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
#ifdef HAVE_RENAMEAT
|
||||
if (dir_fd_specified) {
|
||||
|
@ -10613,12 +10600,6 @@ os_symlink_impl(PyObject *module, path_t *src, path_t *dst,
|
|||
|
||||
#else
|
||||
|
||||
if ((src->narrow && dst->wide) || (src->wide && dst->narrow)) {
|
||||
PyErr_SetString(PyExc_ValueError,
|
||||
"symlink: src and dst must be the same type");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
Py_BEGIN_ALLOW_THREADS
|
||||
#ifdef HAVE_SYMLINKAT
|
||||
if (dir_fd != DEFAULT_DIR_FD) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue