From 15e9104d9e98c23a0867f65e67c678e2a842f5d7 Mon Sep 17 00:00:00 2001 From: Logikoma Date: Mon, 3 Aug 2026 10:43:47 -0400 Subject: [PATCH] Test file metadata failure paths Co-authored-by: Andrew Kesterson --- include/akstdlib.h | 32 ++++++++++++++++++++++++++++++++ src/stat.c | 32 ++++++++++++++++++++++++++------ tests/test_stat.c | 37 +++++++++++++++++++++++++++++++++---- 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/include/akstdlib.h b/include/akstdlib.h index 84fef89..aef0773 100644 --- a/include/akstdlib.h +++ b/include/akstdlib.h @@ -76,6 +76,7 @@ #include #include #include +#include #include #include /* off_t, for the aksl_fseeko/aksl_ftello pair. POSIX, like aksl_realpath. */ @@ -810,10 +811,25 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_strhash_djb2_str(const char *str, uint32 /** @} */ +/* ====================================================================== */ +/** @name File and filesystem metadata + * + * The stat family reports into caller-owned POSIX structs rather than a smaller + * library-defined copy: the platform owns the fields, and a wrapper should not + * discard a field merely because this library does not currently use it. libc + * failures retain their errno value as the status, so callers can distinguish + * absent paths from inaccessible ones without parsing an error message. + * @{ + */ +/* ====================================================================== */ + /** * @brief stat(2). * @param[in] pathname Path to inspect. Required. * @param[out] dest File metadata. Required. + * @throws AKERR_NULLPOINTER If pathname or dest is NULL. + * @throws AKERR_IO If stat(2) failed and left errno at 0. + * @throws (errno) The errno stat(2) set, reported directly as the status. * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_stat(const char *pathname, struct stat *dest); @@ -822,6 +838,9 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_stat(const char *pathname, struct stat * * @brief lstat(2), inspecting a symbolic link itself. * @param[in] pathname Path to inspect. Required. * @param[out] dest File metadata. Required. + * @throws AKERR_NULLPOINTER If pathname or dest is NULL. + * @throws AKERR_IO If lstat(2) failed and left errno at 0. + * @throws (errno) The errno lstat(2) set, reported directly as the status. * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_lstat(const char *pathname, struct stat *dest); @@ -830,6 +849,9 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_lstat(const char *pathname, struct stat * @brief fstat(2). * @param[in] fd Open file descriptor. * @param[out] dest File metadata. Required. + * @throws AKERR_NULLPOINTER If dest is NULL. + * @throws AKERR_IO If fstat(2) failed and left errno at 0. + * @throws (errno) The errno fstat(2) set, reported directly as the status. * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstat(int fd, struct stat *dest); @@ -840,6 +862,9 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstat(int fd, struct stat *dest); * @param[in] pathname Path to inspect. Required. * @param[out] dest File metadata. Required. * @param[in] flags libc fstatat flags, passed unchanged. + * @throws AKERR_NULLPOINTER If pathname or dest is NULL. + * @throws AKERR_IO If fstatat(2) failed and left errno at 0. + * @throws (errno) The errno fstatat(2) set, reported directly as the status. * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatat(int dirfd, const char *pathname, struct stat *dest, int flags); @@ -848,6 +873,9 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatat(int dirfd, const char *pathname, * @brief statvfs(3). * @param[in] path Path on the filesystem. Required. * @param[out] dest Filesystem metadata. Required. + * @throws AKERR_NULLPOINTER If path or dest is NULL. + * @throws AKERR_IO If statvfs(3) failed and left errno at 0. + * @throws (errno) The errno statvfs(3) set, reported directly as the status. * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_statvfs(const char *path, struct statvfs *dest); @@ -856,11 +884,15 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_statvfs(const char *path, struct statvfs * @brief fstatvfs(3). * @param[in] fd Open file descriptor. * @param[out] dest Filesystem metadata. Required. + * @throws AKERR_NULLPOINTER If dest is NULL. + * @throws AKERR_IO If fstatvfs(3) failed and left errno at 0. + * @throws (errno) The errno fstatvfs(3) set, reported directly as the status. * @return NULL on success, an error context otherwise. */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatvfs(int fd, struct statvfs *dest); +/** @} */ /* ====================================================================== */ /** @name Streams: open, read, write, close * diff --git a/src/stat.c b/src/stat.c index 29e799c..82f3b6f 100644 --- a/src/stat.c +++ b/src/stat.c @@ -1,6 +1,20 @@ -/* sys/stat.h and sys/statvfs.h metadata wrappers. */ +/* + * sys/stat.h and sys/statvfs.h metadata wrappers. + * + * These calls make information about a file or filesystem available only by a + * return value that must be checked alongside a caller-owned POSIX struct. The + * wrappers put failure in the return value, where AKERR_NOIGNORE prevents it + * from being silently dropped, while preserving the errno that distinguishes + * missing paths, inaccessible paths, and invalid descriptors. errno is cleared + * immediately before each libc call, so a broken libc that reports failure + * without setting errno is still an AKERR_IO failure rather than status 0. + */ #include + #include +#include +#include + #include "aksl_internal.h" /* @@ -41,8 +55,11 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_lstat(const char *pathname, struct stat */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstat(int fd, struct stat *dest) { - PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "fd=%d, dest=%p", fd, (void *)dest); - errno = 0; FAIL_NONZERO_RETURN(e, fstat(fd, dest), AKSL_ERRNO_OR(AKERR_IO), "fd=%d", fd); SUCCEED_RETURN(e); + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "fd=%d, dest=%p", fd, (void *)dest); + errno = 0; + FAIL_NONZERO_RETURN(e, fstat(fd, dest), AKSL_ERRNO_OR(AKERR_IO), "fd=%d", fd); + SUCCEED_RETURN(e); } /* @@ -57,7 +74,7 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatat(int dirfd, const char *pathname, FAIL_ZERO_RETURN(e, pathname, AKERR_NULLPOINTER, "dirfd=%d, pathname=%p, dest=%p", dirfd, (void *)pathname, (void *)dest); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "dirfd=%d, pathname=%p, dest=%p", dirfd, (void *)pathname, (void *)dest); errno = 0; - FAIL_NONZERO_RETURN(e, fstatat(dirfd, pathname, dest, flags), AKSL_ERRNO_OR(AKERR_IO), "dirfd=%d pathname=%s flags=%d", dirfd, pathname, flags); + FAIL_NONZERO_RETURN(e, fstatat(dirfd, pathname, dest, flags), AKSL_ERRNO_OR(AKERR_IO), "dirfd=%d, pathname=%s, flags=0x%x", dirfd, pathname, flags); SUCCEED_RETURN(e); } @@ -83,6 +100,9 @@ akerr_ErrorContext AKERR_NOIGNORE *aksl_statvfs(const char *path, struct statvfs */ akerr_ErrorContext AKERR_NOIGNORE *aksl_fstatvfs(int fd, struct statvfs *dest) { - PREPARE_ERROR(e); FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "fd=%d, dest=%p", fd, (void *)dest); - errno = 0; FAIL_NONZERO_RETURN(e, fstatvfs(fd, dest), AKSL_ERRNO_OR(AKERR_IO), "fd=%d", fd); SUCCEED_RETURN(e); + PREPARE_ERROR(e); + FAIL_ZERO_RETURN(e, dest, AKERR_NULLPOINTER, "fd=%d, dest=%p", fd, (void *)dest); + errno = 0; + FAIL_NONZERO_RETURN(e, fstatvfs(fd, dest), AKSL_ERRNO_OR(AKERR_IO), "fd=%d", fd); + SUCCEED_RETURN(e); } diff --git a/tests/test_stat.c b/tests/test_stat.c index 14d4244..b7d7950 100644 --- a/tests/test_stat.c +++ b/tests/test_stat.c @@ -2,6 +2,14 @@ #include "aksl_capture.h" #include #include +#include + +/* + * A bit libc has never assigned in the AT_ space. fstatat(2) must reject it + * with EINVAL rather than ignoring it, which is what proves flags reach libc + * unmasked. Re-check this constant if AT_ ever grows into 0x40000000. + */ +#define AKSL_TEST_AT_INVALID 0x40000000 static int test_stat_success_and_fstat(void) { @@ -30,6 +38,7 @@ static int test_stat_success_and_fstat(void) /* A descriptor that was just closed must surface libc's EBADF. */ AKSL_CHECK_STATUS(aksl_fstat(fd, &fd_dest), EBADF); + AKSL_CHECK_STATUS(aksl_fstatvfs(fd, &vfs_dest), EBADF); AKSL_CHECK(unlink(path) == 0); return 0; } @@ -38,15 +47,21 @@ static int test_stat_paths_and_fstatat(void) { char path[AKSL_TMP_MAX]; char child[AKSL_TMP_MAX * 2]; + char directory[AKSL_TMP_MAX]; char linkpath[AKSL_TMP_MAX * 2]; + char *basename = NULL; + int dirfd = -1; struct stat dest; struct stat ldest; + struct stat path_dest; struct statvfs vdest; /* Missing paths and a regular file used as a directory preserve errno. */ AKSL_CHECK_STATUS(aksl_stat("/nonexistent/aksl/stat", &dest), ENOENT); + AKSL_CHECK_STATUS(aksl_lstat("/nonexistent/aksl/stat", &ldest), ENOENT); /* The temporary path is a regular file, so adding a child tests ENOTDIR. */ AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0); AKSL_CHECK(snprintf(child, sizeof(child), "%s/child", path) < (int)sizeof(child)); + AKSL_CHECK_OK(aksl_stat(path, &path_dest)); AKSL_CHECK_STATUS(aksl_stat(child, &dest), ENOTDIR); AKSL_CHECK(snprintf(linkpath, sizeof(linkpath), "%s.link", path) < (int)sizeof(linkpath)); AKSL_CHECK(symlink(path, linkpath) == 0); @@ -57,13 +72,27 @@ static int test_stat_paths_and_fstatat(void) AKSL_CHECK(S_ISREG(dest.st_mode)); AKSL_CHECK(S_ISLNK(ldest.st_mode)); - /* AT_FDCWD makes fstatat resolve this path from the current directory. */ - AKSL_CHECK_OK(aksl_fstatat(AT_FDCWD, path, &dest, 0)); + /* A real dirfd must resolve a relative name against that directory. */ + AKSL_CHECK(snprintf(directory, sizeof(directory), "%s", path) < (int)sizeof(directory)); + basename = strrchr(directory, '/'); + AKSL_CHECK(basename != NULL); + *basename = '\0'; + dirfd = open(directory, O_RDONLY | O_DIRECTORY); + AKSL_CHECK(dirfd >= 0); + AKSL_CHECK_OK(aksl_fstatat(dirfd, strrchr(path, '/') + 1, &dest, 0)); + AKSL_CHECK(dest.st_ino == path_dest.st_ino); + AKSL_CHECK(close(dirfd) == 0); + + /* A valid non-zero flag must reach libc, making this call an lstat. */ + AKSL_CHECK_OK(aksl_fstatat(AT_FDCWD, linkpath, &dest, AT_SYMLINK_NOFOLLOW)); + AKSL_CHECK(S_ISLNK(dest.st_mode)); /* Invalid flags must replace stale errno with the EINVAL libc reports. */ errno = E2BIG; - AKSL_CHECK_STATUS(aksl_fstatat(AT_FDCWD, path, &dest, 0x40000000), EINVAL); - AKSL_CHECK(errno == EINVAL); + AKSL_CHECK_STATUS(aksl_fstatat(AT_FDCWD, path, &dest, AKSL_TEST_AT_INVALID), EINVAL); + + /* statvfs reports ENOENT before it can describe a missing filesystem path. */ + AKSL_CHECK_STATUS(aksl_statvfs("/nonexistent/aksl/stat", &vdest), ENOENT); /* statvfs reports the filesystem containing the current directory. */ AKSL_CHECK_OK(aksl_statvfs(".", &vdest));