// SPDX-License-Identifier: GPL-2.0 #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "utils.h" ssize_t read_nointr(int fd, void *buf, size_t count) { ssize_t ret; do { ret = read(fd, buf, count); } while (ret < 0 && errno == EINTR); return ret; } ssize_t write_nointr(int fd, const void *buf, size_t count) { ssize_t ret; do { ret = write(fd, buf, count); } while (ret < 0 && errno == EINTR); return ret; } #define __STACK_SIZE (8 * 1024 * 1024) pid_t do_clone(int (*fn)(void *), void *arg, int flags) { void *stack; stack = malloc(__STACK_SIZE); if (!stack) return -ENOMEM; #ifdef __ia64__ return __clone2(fn, stack, __STACK_SIZE, flags | SIGCHLD, arg, NULL); #else return clone(fn, stack + __STACK_SIZE, flags | SIGCHLD, arg, NULL); #endif } static int get_userns_fd_cb(void *data) { for (;;) pause(); _exit(0); } int wait_for_pid(pid_t pid) { int status, ret; again: ret = waitpid(pid, &status, 0); if (ret == -1) { if (errno == EINTR) goto again; return -1; } if (!WIFEXITED(status)) return -1; return WEXITSTATUS(status); } static int write_id_mapping(idmap_type_t map_type, pid_t pid, const char *buf, size_t buf_size) { int fd = -EBADF, setgroups_fd = -EBADF; int fret = -1; int ret; char path[STRLITERALLEN("/proc/") + INTTYPE_TO_STRLEN(pid_t) + STRLITERALLEN("/setgroups") + 1]; if (geteuid() != 0 && map_type == ID_TYPE_GID) { ret = snprintf(path, sizeof(path), "/proc/%d/setgroups", pid); if (ret < 0 || ret >= sizeof(path)) goto out; setgroups_fd = open(path, O_WRONLY | O_CLOEXEC); if (setgroups_fd < 0 && errno != ENOENT) { syserror("Failed to open \"%s\"", path); goto out; } if (setgroups_fd >= 0) { ret = write_nointr(setgroups_fd, "deny\n", STRLITERALLEN("deny\n")); if (ret != STRLITERALLEN("deny\n")) { syserror("Failed to write \"deny\" to \"/proc/%d/setgroups\"", pid); goto out; } } } ret = snprintf(path, sizeof(path), "/proc/%d/%cid_map", pid, map_type == ID_TYPE_UID ? 'u' : 'g'); if (ret < 0 || ret >= sizeof(path)) goto out; fd = open(path, O_WRONLY | O_CLOEXEC); if (fd < 0) { syserror("Failed to open \"%s\"", path); goto out; } ret = write_nointr(fd, buf, buf_size); if (ret != buf_size) { syserror("Failed to write %cid mapping to \"%s\"", map_type == ID_TYPE_UID ? 'u' : 'g', path); goto out; } fret = 0; out: safe_close(fd); safe_close(setgroups_fd); return fret; } static int map_ids_from_idmap(struct list *idmap, pid_t pid) { int fill, left; char mapbuf[4096] = {}; bool had_entry = false; idmap_type_t map_type, u_or_g; if (list_empty(idmap)) return 0; for (map_type = ID_TYPE_UID, u_or_g = 'u'; map_type <= ID_TYPE_GID; map_type++, u_or_g = 'g') { char *pos = mapbuf; int ret; struct list *iterator; list_for_each(iterator, idmap) { struct id_map *map = iterator->elem; if (map->map_type != map_type) continue; had_entry = true; left = 4096 - (pos - mapbuf); fill = snprintf(pos, left, "%u %u %u\n", map->nsid, map->hostid, map->range); /* * The kernel only takes <= 4k for writes to * /proc//{g,u}id_map */ if (fill <= 0 || fill >= left) return syserror_set(-E2BIG, "Too many %cid mappings defined", u_or_g); pos += fill; } if (!had_entry) continue; ret = write_id_mapping(map_type, pid, mapbuf, pos - mapbuf); if (ret < 0) return syserror("Failed to write mapping: %s", mapbuf); memset(mapbuf, 0, sizeof(mapbuf)); } return 0; } #ifdef DEBUG_TRACE static void __print_idmaps(pid_t pid, bool gid) { char path_mapping[STRLITERALLEN("/proc/") + INTTYPE_TO_STRLEN(pid_t) + STRLITERALLEN("/_id_map") + 1]; char *line = NULL; size_t len = 0; int ret; FILE *f; ret = snprintf(path_mapping, sizeof(path_mapping), "/proc/%d/%cid_map", pid, gid ? 'g' : 'u'); if (ret < 0 || (size_t)ret >= sizeof(path_mapping)) return; f = fopen(path_mapping, "r"); if (!f) return; while ((ret = getline(&line, &len, f)) > 0) fprintf(stderr, "%s", line); fclose(f); free(line); } static void print_idmaps(pid_t pid) { __print_idmaps(pid, false); __print_idmaps(pid, true); } #else static void print_idmaps(pid_t pid) { } #endif int get_userns_fd_from_idmap(struct list *idmap) { int ret; pid_t pid; char path_ns[STRLITERALLEN("/proc/") + INTTYPE_TO_STRLEN(pid_t) + STRLITERALLEN("/ns/user") + 1]; pid = do_clone(get_userns_fd_cb, NULL, CLONE_NEWUSER | CLONE_NEWNS); if (pid < 0) return -errno; ret = map_ids_from_idmap(idmap, pid); if (ret < 0) return ret; ret = snprintf(path_ns, sizeof(path_ns), "/proc/%d/ns/user", pid); if (ret < 0 || (size_t)ret >= sizeof(path_ns)) { ret = -EIO; } else { ret = open(path_ns, O_RDONLY | O_CLOEXEC | O_NOCTTY); print_idmaps(pid); } (void)kill(pid, SIGKILL); (void)wait_for_pid(pid); return ret; } int get_userns_fd(unsigned long nsid, unsigned long hostid, unsigned long range) { struct list head, uid_mapl, gid_mapl; struct id_map uid_map = { .map_type = ID_TYPE_UID, .nsid = nsid, .hostid = hostid, .range = range, }; struct id_map gid_map = { .map_type = ID_TYPE_GID, .nsid = nsid, .hostid = hostid, .range = range, }; list_init(&head); uid_mapl.elem = &uid_map; gid_mapl.elem = &gid_map; list_add_tail(&head, &uid_mapl); list_add_tail(&head, &gid_mapl); return get_userns_fd_from_idmap(&head); } bool switch_ids(uid_t uid, gid_t gid) { if (setgroups(0, NULL)) return syserror("failure: setgroups"); if (setresgid(gid, gid, gid)) return syserror("failure: setresgid"); if (setresuid(uid, uid, uid)) return syserror("failure: setresuid"); /* Ensure we can access proc files from processes we can ptrace. */ if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0)) return syserror("failure: make dumpable"); return true; } static int userns_fd_cb(void *data) { struct userns_hierarchy *h = data; char c; int ret; ret = read_nointr(h->fd_event, &c, 1); if (ret < 0) return syserror("failure: read from socketpair"); /* Only switch ids if someone actually wrote a mapping for us. */ if (c == '1') { if (!switch_ids(0, 0)) return syserror("failure: switch ids to 0"); } ret = write_nointr(h->fd_event, "1", 1); if (ret < 0) return syserror("failure: write to socketpair"); ret = create_userns_hierarchy(++h); if (ret < 0) return syserror("failure: userns level %d", h->level); return 0; } int create_userns_hierarchy(struct userns_hierarchy *h) { int fret = -1; char c; int fd_socket[2]; int fd_userns = -EBADF, ret = -1; ssize_t bytes; pid_t pid; char path[256]; if (h->level == MAX_USERNS_LEVEL) return 0; ret = socketpair(AF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, fd_socket); if (ret < 0) return syserror("failure: create socketpair"); /* Note the CLONE_FILES | CLONE_VM when mucking with fds and memory. */ h->fd_event = fd_socket[1]; pid = do_clone(userns_fd_cb, h, CLONE_NEWUSER | CLONE_FILES | CLONE_VM); if (pid < 0) { syserror("failure: userns level %d", h->level); goto out_close; } ret = map_ids_from_idmap(&h->id_map, pid); if (ret < 0) { kill(pid, SIGKILL); syserror("failure: writing id mapping for userns level %d for %d", h->level, pid); goto out_wait; } if (!list_empty(&h->id_map)) bytes = write_nointr(fd_socket[0], "1", 1); /* Inform the child we wrote a mapping. */ else bytes = write_nointr(fd_socket[0], "0", 1); /* Inform the child we didn't write a mapping. */ if (bytes < 0) { kill(pid, SIGKILL); syserror("failure: write to socketpair"); goto out_wait; } /* Wait for child to set*id() and become dumpable. */ bytes = read_nointr(fd_socket[0], &c, 1); if (bytes < 0) { kill(pid, SIGKILL); syserror("failure: read from socketpair"); goto out_wait; } snprintf(path, sizeof(path), "/proc/%d/ns/user", pid); fd_userns = open(path, O_RDONLY | O_CLOEXEC); if (fd_userns < 0) { kill(pid, SIGKILL); syserror("failure: open userns level %d for %d", h->level, pid); goto out_wait; } fret = 0; out_wait: if (!wait_for_pid(pid) && !fret) { h->fd_userns = fd_userns; fd_userns = -EBADF; } out_close: if (fd_userns >= 0) close(fd_userns); close(fd_socket[0]); close(fd_socket[1]); return fret; } int add_map_entry(struct list *head, __u32 id_host, __u32 id_ns, __u32 range, idmap_type_t map_type) { struct list *new_list = NULL; struct id_map *newmap = NULL; newmap = malloc(sizeof(*newmap)); if (!newmap) return -ENOMEM; new_list = malloc(sizeof(struct list)); if (!new_list) { free(newmap); return -ENOMEM; } *newmap = (struct id_map){ .hostid = id_host, .nsid = id_ns, .range = range, .map_type = map_type, }; new_list->elem = newmap; list_add_tail(head, new_list); return 0; } /* __expected_uid_gid - check whether file is owned by the provided uid and gid */ bool __expected_uid_gid(int dfd, const char *path, int flags, uid_t expected_uid, gid_t expected_gid, bool log) { int ret; struct stat st; ret = fstatat(dfd, path, &st, flags); if (ret < 0) return log_errno(false, "failure: fstatat"); if (log && st.st_uid != expected_uid) log_stderr("failure: uid(%d) != expected_uid(%d)", st.st_uid, expected_uid); if (log && st.st_gid != expected_gid) log_stderr("failure: gid(%d) != expected_gid(%d)", st.st_gid, expected_gid); errno = 0; /* Don't report misleading errno. */ return st.st_uid == expected_uid && st.st_gid == expected_gid; } /* caps_down - lower all effective caps */ int caps_down(void) { bool fret = false; #ifdef HAVE_SYS_CAPABILITY_H cap_t caps = NULL; int ret = -1; caps = cap_get_proc(); if (!caps) goto out; ret = cap_clear_flag(caps, CAP_EFFECTIVE); if (ret) goto out; ret = cap_set_proc(caps); if (ret) goto out; fret = true; out: cap_free(caps); #endif return fret; } /* caps_down_fsetid - lower CAP_FSETID effective cap */ int caps_down_fsetid(void) { bool fret = false; #ifdef HAVE_SYS_CAPABILITY_H cap_t caps = NULL; cap_value_t cap = CAP_FSETID; int ret = -1; caps = cap_get_proc(); if (!caps) goto out; ret = cap_set_flag(caps, CAP_EFFECTIVE, 1, &cap, 0); if (ret) goto out; ret = cap_set_proc(caps); if (ret) goto out; fret = true; out: cap_free(caps); #endif return fret; } #ifdef HAVE_LIBURING_H int io_uring_openat_with_creds(struct io_uring *ring, int dfd, const char *path, int cred_id, bool with_link, int *ret_cqe) { struct io_uring_cqe *cqe; struct io_uring_sqe *sqe; int ret, i, to_submit = 1; if (with_link) { sqe = io_uring_get_sqe(ring); if (!sqe) return log_error_errno(-EINVAL, EINVAL, "failure: io_uring_sqe"); io_uring_prep_nop(sqe); sqe->flags |= IOSQE_IO_LINK; sqe->user_data = 1; to_submit++; } sqe = io_uring_get_sqe(ring); if (!sqe) return log_error_errno(-EINVAL, EINVAL, "failure: io_uring_sqe"); io_uring_prep_openat(sqe, dfd, path, O_RDONLY | O_CLOEXEC, 0); sqe->user_data = 2; if (cred_id != -1) sqe->personality = cred_id; ret = io_uring_submit(ring); if (ret != to_submit) { log_stderr("failure: io_uring_submit"); goto out; } for (i = 0; i < to_submit; i++) { ret = io_uring_wait_cqe(ring, &cqe); if (ret < 0) { log_stderr("failure: io_uring_wait_cqe"); goto out; } ret = cqe->res; /* * Make sure caller can identify that this is a proper io_uring * failure and not some earlier error. */ if (ret_cqe) *ret_cqe = ret; io_uring_cqe_seen(ring, cqe); } log_debug("Ran test"); out: return ret; } #endif /* HAVE_LIBURING_H */ /* caps_up - raise all permitted caps */ int caps_up(void) { bool fret = false; #ifdef HAVE_SYS_CAPABILITY_H cap_t caps = NULL; cap_value_t cap; int ret = -1; caps = cap_get_proc(); if (!caps) goto out; for (cap = 0; cap <= CAP_LAST_CAP; cap++) { cap_flag_value_t flag; ret = cap_get_flag(caps, cap, CAP_PERMITTED, &flag); if (ret) { if (errno == EINVAL) break; else goto out; } ret = cap_set_flag(caps, CAP_EFFECTIVE, 1, &cap, flag); if (ret) goto out; } ret = cap_set_proc(caps); if (ret) goto out; fret = true; out: cap_free(caps); #endif return fret; } /* chown_r - recursively change ownership of all files */ int chown_r(int fd, const char *path, uid_t uid, gid_t gid) { int dfd, ret; DIR *dir; struct dirent *direntp; dfd = openat(fd, path, O_CLOEXEC | O_DIRECTORY); if (dfd < 0) return -1; dir = fdopendir(dfd); if (!dir) { close(dfd); return -1; } while ((direntp = readdir(dir))) { struct stat st; if (!strcmp(direntp->d_name, ".") || !strcmp(direntp->d_name, "..")) continue; ret = fstatat(dfd, direntp->d_name, &st, AT_SYMLINK_NOFOLLOW); if (ret < 0 && errno != ENOENT) break; if (S_ISDIR(st.st_mode)) ret = chown_r(dfd, direntp->d_name, uid, gid); else ret = fchownat(dfd, direntp->d_name, uid, gid, AT_SYMLINK_NOFOLLOW); if (ret < 0 && errno != ENOENT) break; } ret = fchownat(fd, path, uid, gid, AT_SYMLINK_NOFOLLOW); closedir(dir); return ret; } /* expected_dummy_vfs_caps_uid - check vfs caps are stored with the provided uid */ bool expected_dummy_vfs_caps_uid(int fd, uid_t expected_uid) { #define __cap_raised_permitted(x, ns_cap_data) \ ((ns_cap_data.data[(x) >> 5].permitted) & (1 << ((x)&31))) struct vfs_ns_cap_data ns_xattr = {}; ssize_t ret; ret = fgetxattr(fd, "security.capability", &ns_xattr, sizeof(ns_xattr)); if (ret < 0 || ret == 0) return false; if (ns_xattr.magic_etc & VFS_CAP_REVISION_3) { if (le32_to_cpu(ns_xattr.rootid) != expected_uid) { errno = EINVAL; log_stderr("failure: rootid(%d) != expected_rootid(%d)", le32_to_cpu(ns_xattr.rootid), expected_uid); } return (le32_to_cpu(ns_xattr.rootid) == expected_uid) && (__cap_raised_permitted(CAP_NET_RAW, ns_xattr) > 0); } else { log_stderr("failure: fscaps version"); } return false; } /* set_dummy_vfs_caps - set dummy vfs caps for the provided uid */ int set_dummy_vfs_caps(int fd, int flags, int rootuid) { #define __raise_cap_permitted(x, ns_cap_data) \ ns_cap_data.data[(x) >> 5].permitted |= (1 << ((x)&31)) struct vfs_ns_cap_data ns_xattr; memset(&ns_xattr, 0, sizeof(ns_xattr)); __raise_cap_permitted(CAP_NET_RAW, ns_xattr); ns_xattr.magic_etc |= VFS_CAP_REVISION_3 | VFS_CAP_FLAGS_EFFECTIVE; ns_xattr.rootid = cpu_to_le32(rootuid); return fsetxattr(fd, "security.capability", &ns_xattr, sizeof(ns_xattr), flags); } bool protected_symlinks_enabled(void) { static int enabled = -1; if (enabled == -1) { int fd; ssize_t ret; char buf[256]; enabled = 0; fd = open("/proc/sys/fs/protected_symlinks", O_RDONLY | O_CLOEXEC); if (fd < 0) return false; ret = read(fd, buf, sizeof(buf)); close(fd); if (ret < 0) return false; if (atoi(buf) >= 1) enabled = 1; } return enabled == 1; } static bool is_xfs(const char *fstype) { static int enabled = -1; if (enabled == -1) enabled = !strcmp(fstype, "xfs"); return enabled; } bool xfs_irix_sgid_inherit_enabled(const char *fstype) { static int enabled = -1; if (enabled == -1) { int fd; ssize_t ret; char buf[256]; enabled = 0; if (is_xfs(fstype)) { fd = open("/proc/sys/fs/xfs/irix_sgid_inherit", O_RDONLY | O_CLOEXEC); if (fd < 0) return false; ret = read(fd, buf, sizeof(buf)); close(fd); if (ret < 0) return false; if (atoi(buf) >= 1) enabled = 1; } } return enabled == 1; } bool expected_file_size(int dfd, const char *path, int flags, off_t expected_size) { int ret; struct stat st; ret = fstatat(dfd, path, &st, flags); if (ret < 0) return log_errno(false, "failure: fstatat"); if (st.st_size != expected_size) return log_errno(false, "failure: st_size(%zu) != expected_size(%zu)", (size_t)st.st_size, (size_t)expected_size); return true; } /* is_setid - check whether file is S_ISUID and S_ISGID */ bool is_setid(int dfd, const char *path, int flags) { int ret; struct stat st; ret = fstatat(dfd, path, &st, flags); if (ret < 0) return false; errno = 0; /* Don't report misleading errno. */ return (st.st_mode & S_ISUID) || (st.st_mode & S_ISGID); } /* is_setgid - check whether file or directory is S_ISGID */ bool is_setgid(int dfd, const char *path, int flags) { int ret; struct stat st; ret = fstatat(dfd, path, &st, flags); if (ret < 0) return false; errno = 0; /* Don't report misleading errno. */ return (st.st_mode & S_ISGID); } /* is_sticky - check whether file is S_ISVTX */ bool is_sticky(int dfd, const char *path, int flags) { int ret; struct stat st; ret = fstatat(dfd, path, &st, flags); if (ret < 0) return false; errno = 0; /* Don't report misleading errno. */ return (st.st_mode & S_ISVTX) > 0; } /*is_ixgrp - check whether file or directory is S_IXGRP */ bool is_ixgrp(int dfd, const char *path, int flags) { int ret; struct stat st; ret = fstatat(dfd, path, &st, flags); if (ret < 0) return false; errno = 0; /* Don't report misleading errno. */ return (st.st_mode & S_IXGRP); } bool switch_resids(uid_t uid, gid_t gid) { if (setresgid(gid, gid, gid)) return log_errno(false, "failure: setregid"); if (setresuid(uid, uid, uid)) return log_errno(false, "failure: setresuid"); if (setfsgid(-1) != gid) return log_errno(false, "failure: setfsgid(-1)"); if (setfsuid(-1) != uid) return log_errno(false, "failure: setfsuid(-1)"); return true; } /* rm_r - recursively remove all files */ int rm_r(int fd, const char *path) { int dfd, ret; DIR *dir; struct dirent *direntp; if (!path || strcmp(path, "") == 0) return -1; dfd = openat(fd, path, O_CLOEXEC | O_DIRECTORY); if (dfd < 0) return -1; dir = fdopendir(dfd); if (!dir) { close(dfd); return -1; } while ((direntp = readdir(dir))) { struct stat st; if (!strcmp(direntp->d_name, ".") || !strcmp(direntp->d_name, "..")) continue; ret = fstatat(dfd, direntp->d_name, &st, AT_SYMLINK_NOFOLLOW); if (ret < 0 && errno != ENOENT) break; if (S_ISDIR(st.st_mode)) ret = rm_r(dfd, direntp->d_name); else ret = unlinkat(dfd, direntp->d_name, 0); if (ret < 0 && errno != ENOENT) break; } ret = unlinkat(fd, path, AT_REMOVEDIR); closedir(dir); return ret; } /* fd_to_fd - transfer data from one fd to another */ int fd_to_fd(int from, int to) { for (;;) { uint8_t buf[PATH_MAX]; uint8_t *p = buf; ssize_t bytes_to_write; ssize_t bytes_read; bytes_read = read_nointr(from, buf, sizeof buf); if (bytes_read < 0) return -1; if (bytes_read == 0) break; bytes_to_write = (size_t)bytes_read; do { ssize_t bytes_written; bytes_written = write_nointr(to, p, bytes_to_write); if (bytes_written < 0) return -1; bytes_to_write -= bytes_written; p += bytes_written; } while (bytes_to_write > 0); } return 0; } bool openat_tmpfile_supported(int dirfd) { int fd = -1; fd = openat(dirfd, ".", O_TMPFILE | O_RDWR, S_IXGRP | S_ISGID); if (fd == -1) { if (errno == ENOTSUP) { errno = 0; /* Don't report misleading errno. */ return false; } else { return log_errno(false, "failure: create"); } } if (close(fd)) log_stderr("failure: close"); return true; } /* * There'll be scenarios where you'll want to see the attributes associated with * a directory tree during debugging or just to make sure things look correct. * Simply uncomment and place the print_r() helper where you need it. */ #ifdef DEBUG_TRACE static int fd_cloexec(int fd, bool cloexec) { int oflags, nflags; oflags = fcntl(fd, F_GETFD, 0); if (oflags < 0) return -errno; if (cloexec) nflags = oflags | FD_CLOEXEC; else nflags = oflags & ~FD_CLOEXEC; if (nflags == oflags) return 0; if (fcntl(fd, F_SETFD, nflags) < 0) return -errno; return 0; } static inline int dup_cloexec(int fd) { int fd_dup; fd_dup = dup(fd); if (fd_dup < 0) return -errno; if (fd_cloexec(fd_dup, true)) { close(fd_dup); return -errno; } return fd_dup; } int print_r(int fd, const char *path) { int ret = 0; int dfd, dfd_dup; DIR *dir; struct dirent *direntp; struct stat st; if (!path || *path == '\0') { char buf[sizeof("/proc/self/fd/") + 30]; ret = snprintf(buf, sizeof(buf), "/proc/self/fd/%d", fd); if (ret < 0 || (size_t)ret >= sizeof(buf)) return -1; /* * O_PATH file descriptors can't be used so we need to re-open * just in case. */ dfd = openat(-EBADF, buf, O_CLOEXEC | O_DIRECTORY, 0); } else { dfd = openat(fd, path, O_CLOEXEC | O_DIRECTORY, 0); } if (dfd < 0) return -1; /* * When fdopendir() below succeeds it assumes ownership of the fd so we * to make sure we always have an fd that fdopendir() can own which is * why we dup() in the case where the caller wants us to operate on the * fd directly. */ dfd_dup = dup_cloexec(dfd); if (dfd_dup < 0) { close(dfd); return -1; } dir = fdopendir(dfd); if (!dir) { close(dfd); close(dfd_dup); return -1; } /* Transfer ownership to fdopendir(). */ dfd = -EBADF; while ((direntp = readdir(dir))) { if (!strcmp(direntp->d_name, ".") || !strcmp(direntp->d_name, "..")) continue; ret = fstatat(dfd_dup, direntp->d_name, &st, AT_SYMLINK_NOFOLLOW); if (ret < 0 && errno != ENOENT) break; ret = 0; if (S_ISDIR(st.st_mode)) ret = print_r(dfd_dup, direntp->d_name); else fprintf(stderr, "mode(%o):uid(%d):gid(%d) -> %d/%s\n", (st.st_mode & ~S_IFMT), st.st_uid, st.st_gid, dfd_dup, direntp->d_name); if (ret < 0 && errno != ENOENT) break; } if (!path || *path == '\0') ret = fstatat(fd, "", &st, AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH); else ret = fstatat(fd, path, &st, AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW); if (!ret) fprintf(stderr, "mode(%o):uid(%d):gid(%d) -> %s\n", (st.st_mode & ~S_IFMT), st.st_uid, st.st_gid, (path && *path) ? path : "(null)"); close(dfd_dup); closedir(dir); return ret; } #endif