All checks were successful
Eighty-five comments cited section numbers -- 1.1, 2.2.6, 3.6 -- from a numbering the file had already abandoned before the move to the tracker. They label completed work, so the pointer was the only wrong part. The citation is removed and the sentence kept, which is what issue #27 recommended: these are labels, not references, and a label carrying a version-dependent pointer goes stale again at the next reorganisation. Where a pointer earns its place it names what actually holds the content now -- UPGRADING.md for the confirmed defects, libakerror #15 for the target namespacing, issue #7 for the mutation survivors. README.md and akstdlib.h sent readers to TODO.md for 'what is still open'; they name the tracker. Verified: cmake --build build && ctest --test-dir build, 19/19. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
200 lines
6.6 KiB
C
200 lines
6.6 KiB
C
/*
|
|
* aksl_realpath and aksl_realpath_alloc.
|
|
*
|
|
* The happy paths compare against realpath(3) itself rather than against a
|
|
* hard-coded string, because $TMPDIR may itself be a symlink (/tmp -> /private/tmp
|
|
* and friends) and the resolved answer is what the platform says it is.
|
|
*
|
|
* The failure cases now pass an *uninitialised* resolved_path on purpose. That
|
|
* used to be the crash case: the wrapper's own error path
|
|
* formatted the buffer with %s while realpath(3) leaves its contents
|
|
* unspecified on failure, so the library read uninitialised memory while
|
|
* reporting an error. The message names only the input path now, and this test
|
|
* is what holds that -- it is meant to be run under the sanitizer build, where a
|
|
* regression is an immediate abort rather than a silent read of stack garbage.
|
|
*/
|
|
|
|
#include "aksl_capture.h"
|
|
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
|
|
static int test_resolves_an_existing_file(void)
|
|
{
|
|
char path[AKSL_TMP_MAX];
|
|
char resolved[PATH_MAX];
|
|
char expected[PATH_MAX];
|
|
|
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
|
memset(resolved, 0x00, sizeof(resolved));
|
|
AKSL_CHECK(realpath(path, expected) != NULL);
|
|
|
|
AKSL_CHECK_OK(aksl_realpath(path, resolved, sizeof(resolved)));
|
|
AKSL_CHECK(strcmp(resolved, expected) == 0);
|
|
AKSL_CHECK(resolved[0] == '/');
|
|
AKSL_CHECK(unlink(path) == 0);
|
|
return 0;
|
|
}
|
|
|
|
/* A symlink resolves to its target, not to itself. */
|
|
static int test_resolves_a_symlink_to_its_target(void)
|
|
{
|
|
char target[AKSL_TMP_MAX];
|
|
char link[AKSL_TMP_MAX];
|
|
char resolved[PATH_MAX];
|
|
char expected[PATH_MAX];
|
|
|
|
AKSL_CHECK(aksl_temp_file(target, sizeof(target)) == 0);
|
|
AKSL_CHECK(aksl_temp_file(link, sizeof(link)) == 0);
|
|
/* mkstemp created the link path as a regular file; symlink needs it gone. */
|
|
AKSL_CHECK(unlink(link) == 0);
|
|
AKSL_CHECK(symlink(target, link) == 0);
|
|
|
|
memset(resolved, 0x00, sizeof(resolved));
|
|
AKSL_CHECK(realpath(target, expected) != NULL);
|
|
AKSL_CHECK_OK(aksl_realpath(link, resolved, sizeof(resolved)));
|
|
AKSL_CHECK(strcmp(resolved, expected) == 0);
|
|
|
|
AKSL_CHECK(unlink(link) == 0);
|
|
AKSL_CHECK(unlink(target) == 0);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* The failure path with a buffer nobody has written to. Uninitialised on
|
|
* purpose: see the header comment. Under ASan/MSan this is the test that fails
|
|
* if the error path ever starts reading resolved_path again.
|
|
*/
|
|
static int test_missing_path_reports_enoent(void)
|
|
{
|
|
char resolved[PATH_MAX];
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
|
aksl_realpath("/nonexistent/aksl/path", resolved, sizeof(resolved)),
|
|
ENOENT, "/nonexistent/aksl/path");
|
|
/* The message must name the path and must not quote the buffer back. */
|
|
AKSL_CHECK(strstr(aksl_last_message, "resolved") == NULL);
|
|
return 0;
|
|
}
|
|
|
|
/* A regular file used as a directory component is ENOTDIR, not ENOENT. */
|
|
static int test_non_directory_component_reports_enotdir(void)
|
|
{
|
|
char path[AKSL_TMP_MAX];
|
|
char child[AKSL_TMP_MAX + 8];
|
|
char resolved[PATH_MAX];
|
|
|
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
|
AKSL_CHECK((size_t)snprintf(child, sizeof(child), "%s/child", path)
|
|
< sizeof(child));
|
|
|
|
AKSL_CHECK_STATUS(aksl_realpath(child, resolved, sizeof(resolved)), ENOTDIR);
|
|
AKSL_CHECK(unlink(path) == 0);
|
|
return 0;
|
|
}
|
|
|
|
/* Two symlinks pointing at each other: the kernel gives up with ELOOP. */
|
|
static int test_symlink_loop_reports_eloop(void)
|
|
{
|
|
char a[AKSL_TMP_MAX];
|
|
char b[AKSL_TMP_MAX];
|
|
char resolved[PATH_MAX];
|
|
|
|
AKSL_CHECK(aksl_temp_file(a, sizeof(a)) == 0);
|
|
AKSL_CHECK(aksl_temp_file(b, sizeof(b)) == 0);
|
|
AKSL_CHECK(unlink(a) == 0);
|
|
AKSL_CHECK(unlink(b) == 0);
|
|
AKSL_CHECK(symlink(a, b) == 0);
|
|
AKSL_CHECK(symlink(b, a) == 0);
|
|
|
|
AKSL_CHECK_STATUS(aksl_realpath(a, resolved, sizeof(resolved)), ELOOP);
|
|
|
|
AKSL_CHECK(unlink(a) == 0);
|
|
AKSL_CHECK(unlink(b) == 0);
|
|
return 0;
|
|
}
|
|
|
|
static int test_rejects_null_arguments(void)
|
|
{
|
|
char resolved[PATH_MAX];
|
|
|
|
memset(resolved, 0x00, sizeof(resolved));
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath(NULL, resolved, sizeof(resolved)),
|
|
AKERR_NULLPOINTER, "path=");
|
|
/*
|
|
* this used to be unchecked, and realpath(path, NULL)
|
|
* allocated a buffer that the wrapper then discarded and leaked.
|
|
*/
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath("/tmp", NULL, PATH_MAX),
|
|
AKERR_NULLPOINTER, "resolved_path=");
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* realpath(3) cannot be told how much room it has, so the only safe answer to an
|
|
* undersized buffer is to refuse before calling it. A caller who gets this back
|
|
* has a bug that would otherwise have been a stack smash.
|
|
*/
|
|
static int test_rejects_a_buffer_below_path_max(void)
|
|
{
|
|
char small[16];
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_realpath("/tmp", small, sizeof(small)),
|
|
AKERR_OUTOFBOUNDS, "PATH_MAX");
|
|
return 0;
|
|
}
|
|
|
|
static int test_alloc_form_resolves_and_hands_over_the_buffer(void)
|
|
{
|
|
char path[AKSL_TMP_MAX];
|
|
char expected[PATH_MAX];
|
|
char *resolved = NULL;
|
|
|
|
AKSL_CHECK(aksl_temp_file(path, sizeof(path)) == 0);
|
|
AKSL_CHECK(realpath(path, expected) != NULL);
|
|
|
|
AKSL_CHECK_OK(aksl_realpath_alloc(path, &resolved));
|
|
AKSL_CHECK(resolved != NULL);
|
|
AKSL_CHECK(strcmp(resolved, expected) == 0);
|
|
/* The buffer is the caller's; releasing it through the library closes the
|
|
* leak that the old NULL-destination path opened. */
|
|
AKSL_CHECK_OK(aksl_free(resolved));
|
|
|
|
AKSL_CHECK(unlink(path) == 0);
|
|
return 0;
|
|
}
|
|
|
|
static int test_alloc_form_reports_failure_and_writes_no_pointer(void)
|
|
{
|
|
char *resolved = (char *)0x1;
|
|
|
|
AKSL_CHECK_STATUS_MSG_CONTAINS(
|
|
aksl_realpath_alloc("/nonexistent/aksl/path", &resolved),
|
|
ENOENT, "/nonexistent/aksl/path");
|
|
/* Cleared before the call, so a failure cannot leave a stale pointer. */
|
|
AKSL_CHECK(resolved == NULL);
|
|
|
|
AKSL_CHECK_STATUS(aksl_realpath_alloc(NULL, &resolved), AKERR_NULLPOINTER);
|
|
AKSL_CHECK_STATUS(aksl_realpath_alloc("/tmp", NULL), AKERR_NULLPOINTER);
|
|
return 0;
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
int failures = 0;
|
|
|
|
akerr_init();
|
|
|
|
AKSL_RUN(failures, test_resolves_an_existing_file);
|
|
AKSL_RUN(failures, test_resolves_a_symlink_to_its_target);
|
|
AKSL_RUN(failures, test_missing_path_reports_enoent);
|
|
AKSL_RUN(failures, test_non_directory_component_reports_enotdir);
|
|
AKSL_RUN(failures, test_symlink_loop_reports_eloop);
|
|
AKSL_RUN(failures, test_rejects_null_arguments);
|
|
AKSL_RUN(failures, test_rejects_a_buffer_below_path_max);
|
|
AKSL_RUN(failures, test_alloc_form_resolves_and_hands_over_the_buffer);
|
|
AKSL_RUN(failures, test_alloc_form_reports_failure_and_writes_no_pointer);
|
|
|
|
AKSL_REPORT(failures);
|
|
}
|