Skip to content

gh-89341: Support creation of a link to the file by fd #136302

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Lib/os.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
35 changes: 35 additions & 0 deletions Lib/test/test_posix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On my Fedora 42, I can use AT_EMPTY_PATH as a regular user. I don't need to be root.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is difficult to check. What getpcaps $$ and cat /proc/$$/status | grep CapEff return to you.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vstinner@mona$ getpcaps $$
103445: cap_wake_alarm=i
vstinner@mona$ cat /proc/$$/status | grep CapEff
CapEff:	0000000000000000

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not look like your shell has such capability. Maybe Fedora has patches that ignores it?

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):
Expand Down
4 changes: 2 additions & 2 deletions Modules/clinic/posixmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 35 additions & 5 deletions Modules/posixmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 ------------------------------------------------------------

Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -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__)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading