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