From ab8190e36f7c80c6ea8c3276e206f42c250d86d0 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 4 Jul 2025 21:39:30 +0300 Subject: [PATCH] gh-89341: Support creation of a link to the file by fd --- Lib/os.py | 1 + Lib/test/test_posix.py | 35 +++++++++++++++++++++++++++++ Modules/clinic/posixmodule.c.h | 4 ++-- Modules/posixmodule.c | 40 +++++++++++++++++++++++++++++----- 4 files changed, 73 insertions(+), 7 deletions(-) diff --git a/Lib/os.py b/Lib/os.py index 12926c832f5ba5..050515eb35afab 100644 --- a/Lib/os.py +++ b/Lib/os.py @@ -152,6 +152,7 @@ def _add(str, fn): _add("HAVE_FPATHCONF", "pathconf") if _exists("statvfs") and _exists("fstatvfs"): # mac os x10.3 _add("HAVE_FSTATVFS", "statvfs") + _add("HAVE_LINKAT_AT_EMPTY_PATH", "link") supports_fd = _set _set = set() diff --git a/Lib/test/test_posix.py b/Lib/test/test_posix.py index 628920e34b586f..b0ebd4175eb62c 100644 --- a/Lib/test/test_posix.py +++ b/Lib/test/test_posix.py @@ -34,6 +34,10 @@ _DUMMY_SYMLINK = os.path.join(tempfile.gettempdir(), os_helper.TESTFN + '-dummy-symlink') +root_in_posix = False +if hasattr(os, 'geteuid'): + root_in_posix = (os.geteuid() == 0) + requires_32b = unittest.skipUnless( # Emscripten/WASI have 32 bits pointers, but support 64 bits syscall args. sys.maxsize < 2**32 and not (support.is_emscripten or support.is_wasi), @@ -1689,6 +1693,37 @@ def test_link_dir_fd(self): self.assertEqual(posix.stat(fullname)[1], posix.stat(fulllinkname)[1]) + @unittest.skipIf( + support.is_wasi, + "WASI: symlink following on path_link is not supported" + ) + @unittest.skipUnless( + hasattr(os, "link") and os.link in os.supports_fd, + "test needs fd support in os.link()" + ) + @unittest.skipUnless(root_in_posix, + "requires the CAP_DAC_READ_SEARCH capability") + def test_link_fd(self): + with self.prepare_file() as (dir_fd, name, fullname), \ + self.prepare() as (dir_fd2, linkname, fulllinkname): + fd = posix.open(fullname, posix.O_PATH) + try: + with self.assertRaises(ValueError): + posix.link(fd, linkname, src_dir_fd=dir_fd, dst_dir_fd=dir_fd2) + with self.assertRaises(FileNotFoundError): + posix.stat(fulllinkname) + + try: + posix.link(fd, linkname, dst_dir_fd=dir_fd2) + except PermissionError as e: + self.skipTest('posix.link(): %s' % e) + self.addCleanup(posix.unlink, fulllinkname) + # should have same inodes + self.assertEqual(posix.stat(fullname)[1], + posix.stat(fulllinkname)[1]) + finally: + posix.close(fd) + @unittest.skipUnless(os.mkdir in os.supports_dir_fd, "test needs dir_fd support in os.mkdir()") def test_mkdir_dir_fd(self): with self.prepare() as (dir_fd, name, fullname): diff --git a/Modules/clinic/posixmodule.c.h b/Modules/clinic/posixmodule.c.h index 3621a0625411d3..6b61970780529f 100644 --- a/Modules/clinic/posixmodule.c.h +++ b/Modules/clinic/posixmodule.c.h @@ -1526,7 +1526,7 @@ os_link(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwn #undef KWTUPLE PyObject *argsbuf[5]; Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 2; - path_t src = PATH_T_INITIALIZE_P("link", "src", 0, 0, 0, 0); + path_t src = PATH_T_INITIALIZE_P("link", "src", 0, 0, 0, HAVE_LINKAT_AT_EMPTY_PATH); 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; @@ -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=ae64df0389746258 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=03f4d119f9d75955 input=a9049054013a1b77]*/ diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index b570f81b7cf7c2..e387359e6af795 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -593,6 +593,12 @@ extern char *ctermid_r(char *); # define HAVE_PTSNAME_R_RUNTIME 1 #endif +#if defined(HAVE_LINKAT) && defined(AT_EMPTY_PATH) +# define HAVE_LINKAT_AT_EMPTY_PATH 1 +#else +# define HAVE_LINKAT_AT_EMPTY_PATH 0 +#endif + // --- os module ------------------------------------------------------------ @@ -4346,7 +4352,7 @@ os_getcwdb_impl(PyObject *module) os.link - src : path_t + src : path_t(allow_fd='HAVE_LINKAT_AT_EMPTY_PATH') dst : path_t * src_dir_fd : dir_fd = None @@ -4369,7 +4375,7 @@ 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=1d5e602d115fed7b]*/ +/*[clinic end generated code: output=7f00f6007fd5269a input=7806074f9b44fb8c]*/ { #ifdef MS_WINDOWS BOOL result = FALSE; @@ -4379,6 +4385,11 @@ os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd, #ifdef HAVE_LINKAT if (HAVE_LINKAT_RUNTIME) { + if ((src_dir_fd != DEFAULT_DIR_FD) && (src->fd != -1)) { + PyErr_SetString(PyExc_ValueError, + "link: can't specify both src_dir_fd and fd"); + return NULL; + } if (follow_symlinks < 0) { follow_symlinks = 1; } @@ -4390,6 +4401,10 @@ os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd, argument_unavailable_error("link", "src_dir_fd and dst_dir_fd"); return NULL; } + if (src->fd != -1) { + argument_unavailable_error("link", "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__) @@ -4427,9 +4442,20 @@ os_link_impl(PyObject *module, path_t *src, path_t *dst, int src_dir_fd, Py_BEGIN_ALLOW_THREADS #ifdef HAVE_LINKAT if (HAVE_LINKAT_RUNTIME) { - result = linkat(src_dir_fd, src->narrow, - dst_dir_fd, dst->narrow, - follow_symlinks ? AT_SYMLINK_FOLLOW : 0); + int flags = follow_symlinks ? AT_SYMLINK_FOLLOW : 0; +#if HAVE_LINKAT_AT_EMPTY_PATH + if (src->fd != -1) { + result = linkat(src->fd, "", + dst_dir_fd, dst->narrow, + flags | AT_EMPTY_PATH); + } + else +#endif + { + result = linkat(src_dir_fd, src->narrow, + dst_dir_fd, dst->narrow, + flags); + } } else #endif @@ -17990,6 +18016,10 @@ static const struct have_function { { "HAVE_LINKAT", probe_linkat }, #endif +#if HAVE_LINKAT_AT_EMPTY_PATH + { "HAVE_LINKAT_AT_EMPTY_PATH", NULL }, +#endif + #ifdef HAVE_LCHFLAGS { "HAVE_LCHFLAGS", NULL }, #endif 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