/* * aksl_realpath and aksl_realpath_alloc -- TODO.md section 1.5, now complete. * * 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 (TODO.md 2.1.6): 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 #include 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="); /* * TODO.md 2.1.6: 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); }