Skip to content
Merged
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
31 changes: 30 additions & 1 deletion test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,31 @@ if(HAVE_COVERAGE_TOOLS)
endif(HAVE_COVERAGE_TOOLS)


# ============================================================================
# test_create_empty_client_keytab
# ============================================================================
# Integration test: run the built create-empty-client-keytab binary and verify
# it creates the per-UID keytab (and refuses to clobber it on a second run).
# This binary performs no hardening in its own code path, so instrumenting and
# running it captures create-empty-client-keytab.c main() in the report the
# same way client-keytab-name is captured above.

add_executable(test_create_empty_client_keytab test_create_empty_client_keytab.c)

target_include_directories(test_create_empty_client_keytab PRIVATE ${TEST_INCLUDES})
target_compile_definitions(test_create_empty_client_keytab PRIVATE
"CLIENT_EMPTY_CLIENT_KEYTAB_BIN=\"$<TARGET_FILE:create-empty-client-keytab>\"")
add_coverage_flags(test_create_empty_client_keytab)

add_test(NAME test_create_empty_client_keytab COMMAND test_create_empty_client_keytab)

# Instrument the create-empty-client-keytab binary itself so its main() and the
# path/validation helpers it calls are captured by the coverage report.
if(HAVE_COVERAGE_TOOLS)
add_coverage_flags(create-empty-client-keytab)
endif(HAVE_COVERAGE_TOOLS)


# ============================================================================
# test_kcron_caps
# ============================================================================
Expand Down Expand Up @@ -217,12 +242,16 @@ set(UNIT_TEST_BINARIES $<TARGET_FILE:test_kcron_filename>
$<TARGET_FILE:test_kcron_keytab>
$<TARGET_FILE:test_kcron_caps>
$<TARGET_FILE:test_kcron_keytab_file>
$<TARGET_FILE:test_client_keytab_name>)
$<TARGET_FILE:test_client_keytab_name>
$<TARGET_FILE:test_create_empty_client_keytab>
$<TARGET_FILE:create-empty-client-keytab>)

add_custom_target(test_binaries DEPENDS test_kcron_filename test_kcron_keytab
test_kcron_caps
test_kcron_keytab_file
test_client_keytab_name
test_create_empty_client_keytab
create-empty-client-keytab
COMMENT "Building every unit test binary")

if(BUILD_TESTING AND HAVE_COVERAGE_TOOLS)
Expand Down
149 changes: 149 additions & 0 deletions test/test_create_empty_client_keytab.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Fermi Forward Discovery Group

/**
* test_create_empty_client_keytab.c - Integration test for the
* create-empty-client-keytab(8) binary.
*
* create-empty-client-keytab derives <base>/<uid>/client.keytab from the
* calling UID, validates that the per-UID directory exists, and creates an
* empty but valid keytab there. It performs no hardening of its own, so the
* built binary can be exercised end to end exactly like client-keytab-name.
*
* The test is environment-adaptive: it works whether or not the per-UID
* directory already exists.
*
* * When the directory exists (or can be created), the first run creates the
* keytab (exit 0, mode 0600) and the second run is refused (exit 1,
* "already exists") - covering the creation path and the O_EXCL refusal.
* * When the directory is absent and cannot be created here (typical for an
* unprivileged user under a root-owned base), the run is refused (exit 1) -
* covering the validate_keytab_directory() rejection path.
*
* The binary path is injected at build time via CLIENT_EMPTY_CLIENT_KEYTAB_BIN.
*/

#define __PROGRAM_NAME "test_create_empty_client_keytab"

/* clang-format off */
#include "autoconf.h"
#include "kcron_filename.h"
/* clang-format on */

#include <sys/stat.h>
#include <unistd.h>

#include "test_framework.h"

#ifndef CLIENT_EMPTY_CLIENT_KEYTAB_BIN
#define CLIENT_EMPTY_CLIENT_KEYTAB_BIN "create-empty-client-keytab"
#endif

/*
* run_binary - Capture the binary's stdout into @out.
* @out: Destination buffer.
* @n: Size of @out.
*
* Returns 0 if the binary exited successfully, EXIT_FAILURE on refusal, or -1
* if the binary could not be launched.
*/
static int run_binary(char *out, size_t n) {
FILE *pipe;
char line[FILE_PATH_MAX_LENGTH + 3];
size_t total = 0;

pipe = popen(CLIENT_EMPTY_CLIENT_KEYTAB_BIN, "r");
if (pipe == NULL) {
return -1;
}

while (fgets(line, sizeof(line), pipe) != NULL) {
size_t linelen = strlen(line);
if (total + linelen >= n) {
break;
}
memcpy(out + total, line, linelen);
total += linelen;
}
out[total] = '\0';

return (pclose(pipe) == 0) ? 0 : EXIT_FAILURE;
}

/*
* create_empty_client_keytab_runs - Drive create-empty-client-keytab end to end.
*
* Covers main(): allocation success, get_filenames(), and either the creation
* success path (validate_keytab_directory + create_keytab_file both return 0)
* with the subsequent "already exists" refusal, or the validate_keytab_directory
* rejection path when the per-UID directory is not available.
*/
TEST(create_empty_client_keytab_runs) {
char keytab_dir[FILE_PATH_MAX_LENGTH + 3];
char keytab_filename[FILE_PATH_MAX_LENGTH + 3];
char keytab[FILE_PATH_MAX_LENGTH + 3];
char out[FILE_PATH_MAX_LENGTH + 3];
struct stat st;
int rc;

(void)memset(keytab_dir, 0, sizeof(keytab_dir));
(void)memset(keytab_filename, 0, sizeof(keytab_filename));
(void)memset(keytab, 0, sizeof(keytab));
(void)memset(out, 0, sizeof(out));

/* Derive the same path the binary will use. */
TEST_ASSERT_EQ(get_filenames(keytab_dir, keytab_filename, keytab), 0, "path derivation must succeed");

/* Start from a known state so the first run exercises the creation path. */
(void)unlink(keytab);

/*
* Ensure the per-UID directory is present. If it already exists we proceed to
* the creation path; if it is missing we try to create it, and if that is not
* permitted we fall back to the rejection assertion below.
*/
if (stat(keytab_dir, &st) != 0) {
if (mkdir(keytab_dir, 0700) != 0) {
/* The per-UID directory does not exist and cannot be created here (the
* base directory is typically not writable by an unprivileged user). The
* binary must reject the missing directory. */
rc = run_binary(out, sizeof(out));
TEST_ASSERT_EQ(rc, EXIT_FAILURE, "a missing per-UID directory must be rejected");
return;
}
}

/* Per-UID directory present: the first run creates the keytab. */
rc = run_binary(out, sizeof(out));
TEST_ASSERT_EQ(rc, 0, "creation must succeed when the per-UID directory exists");

/* The keytab must now exist at the derived path, be a regular file, and be
* created mode 0600. */
TEST_ASSERT_EQ(stat(keytab, &st), 0, "the keytab must be created");
if (stat(keytab, &st) == 0) {
TEST_ASSERT_EQ((int)(S_ISREG(st.st_mode)), 1, "created entry must be a regular file");
TEST_ASSERT_EQ((int)(st.st_mode & 07777), 0600, "the keytab must be mode 0600");
}

/* A second run must refuse to clobber the now-existing keytab (O_EXCL). */
rc = run_binary(out, sizeof(out));
TEST_ASSERT_EQ(rc, EXIT_FAILURE, "recreating an existing keytab must be rejected");

/* Clean up so later runs start from the same known state. */
(void)unlink(keytab);
}

/* ============================================================================
* Test Runner
* ============================================================================ */

int main(int argc, char **argv) {
int result;

TEST_INIT(10, false, false); /* timeout, verbose, duration */

RUN_TEST(create_empty_client_keytab_runs);

result = TEST_EXECUTE();
return result;
}
57 changes: 57 additions & 0 deletions test/test_kcron_filename.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@

#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#include "test_framework.h"

Expand Down Expand Up @@ -165,6 +167,55 @@ TEST(get_filenames_path_ends_with_filename) {
TEST_ASSERT_EQ(strncmp(keytab + (path_len - name_len), keytab_filename, name_len), 0, "path must end with the keytab filename");
}

/*
* expect_exit - Fork, run @fn in the child, and report success in the parent.
* @fn: function to invoke that is expected to call exit() (never returns).
*
* get_client_dirname() and get_filenames() call exit() on their NULL-pointer
* guards. A forked child letting those functions exit would fail the test, so
* each guard runs in a grandchild whose non-zero termination is captured here.
*/
static void expect_exit(void (*fn)(void)) {
pid_t pid = fork();
if (pid == 0) {
fn();
/* Unreachable: every guarded path calls exit(). */
_exit(7);
}
int status = 0;
waitpid(pid, &status, 0);
TEST_ASSERT_NOT_EQ(status, 0, "the guarded helper must terminate via exit()");
}

static void thunks_client_dirname_null(void) {
const int rc = get_client_dirname(NULL);
(void)rc;
}
static void thunks_filenames_dir_null(void) {
char name[FILE_PATH_MAX_LENGTH];
const int rc = get_filenames(NULL, name, name);
(void)rc;
}
static void thunks_filenames_name_null(void) {
char dir[FILE_PATH_MAX_LENGTH];
const int rc = get_filenames(dir, NULL, NULL);
(void)rc;
}
static void thunks_filenames_path_null(void) {
char dir[FILE_PATH_MAX_LENGTH];
char name[FILE_PATH_MAX_LENGTH];
const int rc = get_filenames(dir, name, NULL);
(void)rc;
}

TEST(client_dirname_null_is_rejected) { expect_exit(thunks_client_dirname_null); }

TEST(filenames_dir_null_is_rejected) { expect_exit(thunks_filenames_dir_null); }

TEST(filenames_name_null_is_rejected) { expect_exit(thunks_filenames_name_null); }

TEST(filenames_path_null_is_rejected) { expect_exit(thunks_filenames_path_null); }

/* ============================================================================
* Test Runner
* ============================================================================
Expand All @@ -186,6 +237,12 @@ int main(int argc, char **argv) {
RUN_TEST(get_filenames_reproducible);
RUN_TEST(get_filenames_path_ends_with_filename);

/* get_client_dirname / get_filenames NULL guards (each exits on failure) */
RUN_TEST(client_dirname_null_is_rejected);
RUN_TEST(filenames_dir_null_is_rejected);
RUN_TEST(filenames_name_null_is_rejected);
RUN_TEST(filenames_path_null_is_rejected);

result = TEST_EXECUTE();
return result;
}
52 changes: 52 additions & 0 deletions test/test_kcron_keytab.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>

#include "test_framework.h"
Expand Down Expand Up @@ -114,6 +115,51 @@ TEST(write_empty_keytab_writes_only_two_bytes) {
TEST_ASSERT_EQ(nread, 2, "file must contain exactly two bytes");
}

/*
* write_empty_keytab() rejects a non-positive or stdio file descriptor before
* writing anything. Those guards call exit(), so each is exercised in a
* grandchild whose non-zero termination is captured here rather than allowed to
* fail the enclosing test.
*
* Note: the descriptor is validated before any write, so passing fd 0/1/2 never
* touches the inherited /dev/null-backed stdout/stderr.
*/
static void expect_exit_write(void (*fn)(void)) {
pid_t pid = fork();
if (pid == 0) {
fn();
_exit(7);
}
int status = 0;
waitpid(pid, &status, 0);
TEST_ASSERT_NOT_EQ(status, 0, "write_empty_keytab must reject the descriptor");
}

static void guard_neg(void) {
const int rc = write_empty_keytab(-1);
(void)rc;
}
static void guard_stdin(void) {
const int rc = write_empty_keytab(0);
(void)rc;
}
static void guard_stdout(void) {
const int rc = write_empty_keytab(1);
(void)rc;
}
static void guard_stderr(void) {
const int rc = write_empty_keytab(2);
(void)rc;
}

TEST(write_empty_keytab_neg_descriptor_rejected) { expect_exit_write(guard_neg); }

TEST(write_empty_keytab_stdin_descriptor_rejected) { expect_exit_write(guard_stdin); }

TEST(write_empty_keytab_stdout_descriptor_rejected) { expect_exit_write(guard_stdout); }

TEST(write_empty_keytab_stderr_descriptor_rejected) { expect_exit_write(guard_stderr); }

/* ============================================================================
* Test Runner
* ============================================================================
Expand All @@ -128,6 +174,12 @@ int main(int argc, char **argv) {
RUN_TEST(write_empty_keytab_writes_exact_magic);
RUN_TEST(write_empty_keytab_writes_only_two_bytes);

/* write_empty_keytab descriptor guards (each exits on failure) */
RUN_TEST(write_empty_keytab_neg_descriptor_rejected);
RUN_TEST(write_empty_keytab_stdin_descriptor_rejected);
RUN_TEST(write_empty_keytab_stdout_descriptor_rejected);
RUN_TEST(write_empty_keytab_stderr_descriptor_rejected);

result = TEST_EXECUTE();
return result;
}
Loading
Loading