feat(scheduler-core): implement recording directory provisioning and path validation (task 9.1)
Adds --record-dir CLI flag and sched_validate_record_filename() with canonical-path containment (realpath()-based, not textual-prefix) plus separator/".." rejection before any filesystem access. Adversarial review found and this task fixed two CWE-59 inode-aliasing bypasses: a direct hard link inside record_dir aliasing an outside file, and a compound variant where a symlink pointed at an in-directory file itself hard-linked outside record_dir. Both closed via st_nlink checks on the direct entry and the fully-resolved symlink target respectively, each verified with a live RED->GREEN reproduction. TOCTOU residual risk documented and deferred to task 9.2 (O_NOFOLLOW + fstat() re-check). 18/18 recording subtests, 24/24 scheduler suite, clang-format clean.
This commit is contained in:
parent
31ef5d7093
commit
4e5547ee55
8 changed files with 889 additions and 30 deletions
|
|
@ -393,6 +393,25 @@ sched_snapshot_save_load_test = executable(
|
|||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
# White-box test for the recording-directory path-validation function
|
||||
# (scheduler-core spec, task 9.1, Req 5 AC 10 "Record file path
|
||||
# validation"): rejects a recordFile containing a path separator or a ".."
|
||||
# segment BEFORE any filesystem access, and confirms containment beneath
|
||||
# record_dir via a genuine canonical-path (realpath()) comparison rather
|
||||
# than a textual prefix match -- including a symlink-escape case and the
|
||||
# "record_dir vs record_dir-evil sibling" prefix-match trap named in this
|
||||
# task's own security brief. Same white-box #include pattern as
|
||||
# test_job_update.c/test_handlers.c (pulls the real
|
||||
# ../src/recording.c into this TU) -- the function under test has no
|
||||
# scheduler-struct fixture dependency at all, so this is the simplest
|
||||
# white-box test in this directory.
|
||||
sched_recording_path_validation_test = executable(
|
||||
'sched_recording_path_validation_test',
|
||||
['test_recording_path_validation.c'],
|
||||
include_directories:[incdir],
|
||||
dependencies: [scheduler_test_deps],
|
||||
install: false)
|
||||
|
||||
# Register as meson tests so `meson test --suite scheduler` runs them all.
|
||||
# detect_leaks=0 matches the suite convention (fixtures are not torn down).
|
||||
test('scheduler test', scheduler_test, suite: 'scheduler',
|
||||
|
|
@ -453,3 +472,6 @@ test('scheduler driver session ordering test',
|
|||
test('scheduler snapshot save load test',
|
||||
sched_snapshot_save_load_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
test('scheduler recording path validation test',
|
||||
sched_recording_path_validation_test, suite: 'scheduler',
|
||||
env: ['ASAN_OPTIONS=detect_leaks=0'])
|
||||
|
|
|
|||
487
src/scheduler/testsuite/test_recording_path_validation.c
Normal file
487
src/scheduler/testsuite/test_recording_path_validation.c
Normal file
|
|
@ -0,0 +1,487 @@
|
|||
/*
|
||||
* test_recording_path_validation.c - contract test for
|
||||
* sched_validate_record_filename() (scheduler-core spec, task 9.1, Req 5
|
||||
* AC 10 "Record file path validation").
|
||||
*
|
||||
* Observable under test:
|
||||
* - A filename containing a path separator ('/') is rejected (-EINVAL)
|
||||
* before ANY filesystem call is made -- proven by pairing a malicious
|
||||
* filename with a record_dir that does not exist on disk: if the
|
||||
* function had proceeded past the string check, resolving that
|
||||
* nonexistent record_dir would have failed with -ENOENT instead. Only
|
||||
* -EINVAL coming back proves the string check short-circuited first.
|
||||
* - A filename containing a ".." substring -- leading, trailing,
|
||||
* embedded, or doubled -- is rejected the same way, same proof.
|
||||
* - A record_dir containing a pre-existing symlink whose target escapes
|
||||
* record_dir is REJECTED even when the filename component looks
|
||||
* perfectly safe (no '/', no ".."), via a genuine canonical-path
|
||||
* (realpath()) comparison -- not a textual prefix match. A dedicated
|
||||
* test also proves the comparison isn't a naive strncmp() prefix
|
||||
* check by using a sibling directory whose name textually starts with
|
||||
* record_dir's own canonical path (the "/var/log/xce-evil" vs
|
||||
* "/var/log/xce" trap called out in this task's own security brief).
|
||||
* - A legitimate bare filename resolves to record_dir/filename and is
|
||||
* accepted.
|
||||
*
|
||||
* White-box #include of ../src/recording.c (same convention as every
|
||||
* other white-box test in this directory) -- the function under test has
|
||||
* no internal state or fixture dependencies, so this is purely for the
|
||||
* standalone-executable convention this testsuite already establishes
|
||||
* (e.g. test_job_update.c, test_picker_iface.c), not because anything
|
||||
* static/internal needs exercising beyond the one exported function.
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
/* Pull in the real sched_validate_record_filename() under test. */
|
||||
#include "../src/recording.c"
|
||||
|
||||
void setUp(void) { return; }
|
||||
void tearDown(void) { return; }
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Helpers */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void make_tmp_dir(char* out, size_t out_len, const char* suffix)
|
||||
{
|
||||
char tmpl[PATH_MAX];
|
||||
|
||||
/* mkdtemp() requires the template's LAST SIX characters to be
|
||||
* "XXXXXX" -- suffix must precede it, not follow it. */
|
||||
snprintf(tmpl, sizeof(tmpl), "/tmp/sched_recval%s_XXXXXX", suffix);
|
||||
TEST_ASSERT_NOT_NULL(mkdtemp(tmpl));
|
||||
TEST_ASSERT_TRUE(strlen(tmpl) < out_len);
|
||||
strcpy(out, tmpl);
|
||||
}
|
||||
|
||||
static void touch_file(const char* path)
|
||||
{
|
||||
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
TEST_ASSERT_TRUE(fd >= 0);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* NULL / basic argument validation */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_null_record_dir_returns_einval(void)
|
||||
{
|
||||
char out[PATH_MAX];
|
||||
int rc
|
||||
= sched_validate_record_filename(NULL, "foo.replay", out, sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
||||
}
|
||||
|
||||
static void test_null_filename_returns_einval(void)
|
||||
{
|
||||
char out[PATH_MAX];
|
||||
int rc = sched_validate_record_filename("/tmp", NULL, out, sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
||||
}
|
||||
|
||||
static void test_null_out_path_returns_einval(void)
|
||||
{
|
||||
int rc = sched_validate_record_filename("/tmp", "foo.replay", NULL, 64);
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
||||
}
|
||||
|
||||
static void test_zero_out_path_len_returns_einval(void)
|
||||
{
|
||||
char out[PATH_MAX];
|
||||
int rc = sched_validate_record_filename("/tmp", "foo.replay", out, 0);
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
||||
}
|
||||
|
||||
static void test_empty_filename_returns_einval(void)
|
||||
{
|
||||
char out[PATH_MAX];
|
||||
int rc = sched_validate_record_filename("/tmp", "", out, sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT(-EINVAL, rc);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Path-separator rejection -- BEFORE any filesystem access (Req 5 AC 10) */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_filename_with_slash_rejected_before_filesystem_access(void)
|
||||
{
|
||||
/* record_dir deliberately does NOT exist -- if the implementation had
|
||||
* proceeded past the string check, realpath(record_dir) would fail
|
||||
* with -ENOENT. Getting -EINVAL back instead proves the '/' check
|
||||
* short-circuited first, with zero filesystem calls on the malicious
|
||||
* string (or even on record_dir). */
|
||||
static const char* bad_names[]
|
||||
= { "/etc/passwd", "evil/", "a/b", "../../etc/passwd", "/" };
|
||||
char out[PATH_MAX];
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < sizeof(bad_names) / sizeof(bad_names[0]); i++) {
|
||||
int rc = sched_validate_record_filename(
|
||||
"/definitely/does/not/exist/xyz123", bad_names[i], out,
|
||||
sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(-EINVAL, rc, bad_names[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* ".." rejection, all positions -- BEFORE any filesystem access */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_dotdot_all_positions_rejected_before_filesystem_access(void)
|
||||
{
|
||||
static const char* bad_names[] = {
|
||||
"..", /* bare */
|
||||
"...", /* tripled dot, still contains ".." */
|
||||
"....", /* quadrupled */
|
||||
"..evil", /* leading */
|
||||
"evil..", /* trailing */
|
||||
"ev..il", /* embedded */
|
||||
"a..b..c" /* doubled */
|
||||
};
|
||||
char out[PATH_MAX];
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < sizeof(bad_names) / sizeof(bad_names[0]); i++) {
|
||||
int rc = sched_validate_record_filename(
|
||||
"/definitely/does/not/exist/xyz123", bad_names[i], out,
|
||||
sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(-EINVAL, rc, bad_names[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* record_dir configuration errors (distinct from malicious-filename path) */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_nonexistent_record_dir_with_valid_filename_returns_error(void)
|
||||
{
|
||||
/* A perfectly well-formed filename still fails once the (nonexistent)
|
||||
* record_dir itself is resolved -- proves this is a DIFFERENT error
|
||||
* path than the string-rejection tests above (which never reach
|
||||
* realpath() at all). */
|
||||
char out[PATH_MAX];
|
||||
int rc = sched_validate_record_filename("/definitely/does/not/exist/xyz123",
|
||||
"session.replay", out, sizeof(out));
|
||||
TEST_ASSERT_TRUE(rc < 0);
|
||||
TEST_ASSERT_NOT_EQUAL(-EINVAL, rc);
|
||||
}
|
||||
|
||||
static void test_record_dir_that_is_a_file_returns_enotdir(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
char file_path[PATH_MAX];
|
||||
char out[PATH_MAX];
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_parent");
|
||||
snprintf(file_path, sizeof(file_path), "%s/not_a_dir", record_dir);
|
||||
touch_file(file_path);
|
||||
|
||||
rc = sched_validate_record_filename(file_path, "session.replay", out,
|
||||
sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT(-ENOTDIR, rc);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Legitimate filename acceptance */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_legitimate_bare_filename_resolves_inside_record_dir(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
char resolved_dir[PATH_MAX];
|
||||
char out[PATH_MAX];
|
||||
char expected[PATH_MAX];
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_ok");
|
||||
TEST_ASSERT_NOT_NULL(realpath(record_dir, resolved_dir));
|
||||
|
||||
rc = sched_validate_record_filename(record_dir, "session.replay", out,
|
||||
sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
snprintf(expected, sizeof(expected), "%s/session.replay", resolved_dir);
|
||||
TEST_ASSERT_EQUAL_STRING(expected, out);
|
||||
}
|
||||
|
||||
static void test_legitimate_filename_overwriting_existing_regular_file_ok(void)
|
||||
{
|
||||
/* An existing regular file with the same name is a legitimate
|
||||
* overwrite target (task 9.2's fopen() will truncate it), not an
|
||||
* error. */
|
||||
char record_dir[PATH_MAX];
|
||||
char resolved_dir[PATH_MAX];
|
||||
char existing[PATH_MAX];
|
||||
char out[PATH_MAX];
|
||||
char expected[PATH_MAX];
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_overwrite");
|
||||
TEST_ASSERT_NOT_NULL(realpath(record_dir, resolved_dir));
|
||||
snprintf(existing, sizeof(existing), "%s/session.replay", resolved_dir);
|
||||
touch_file(existing);
|
||||
|
||||
rc = sched_validate_record_filename(record_dir, "session.replay", out,
|
||||
sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
snprintf(expected, sizeof(expected), "%s/session.replay", resolved_dir);
|
||||
TEST_ASSERT_EQUAL_STRING(expected, out);
|
||||
}
|
||||
|
||||
static void test_out_path_buffer_too_small_returns_enametoolong(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
char out[4]; /* far too small for any real resolved path */
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_small");
|
||||
|
||||
rc = sched_validate_record_filename(record_dir, "session.replay", out,
|
||||
sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT(-ENAMETOOLONG, rc);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Symlink-escape containment -- canonical-path comparison, not textual */
|
||||
/* prefix matching (Req 5 AC 10's core security property) */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_symlink_inside_record_dir_escaping_outside_is_rejected(void)
|
||||
{
|
||||
/* record_dir contains a symlink (named exactly like a legitimate,
|
||||
* innocent-looking filename) whose target lives OUTSIDE record_dir.
|
||||
* The filename component itself violates neither the '/' nor the
|
||||
* ".." rule -- only the canonical-path containment check on the
|
||||
* pre-existing symlink's resolved target can catch this. */
|
||||
char record_dir[PATH_MAX];
|
||||
char outside_dir[PATH_MAX];
|
||||
char outside_target[PATH_MAX];
|
||||
char link_path[PATH_MAX];
|
||||
char out[PATH_MAX];
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_escsrc");
|
||||
make_tmp_dir(outside_dir, sizeof(outside_dir), "_escdst");
|
||||
|
||||
snprintf(outside_target, sizeof(outside_target), "%s/secret", outside_dir);
|
||||
touch_file(outside_target);
|
||||
|
||||
snprintf(link_path, sizeof(link_path), "%s/looks_safe", record_dir);
|
||||
TEST_ASSERT_EQUAL_INT(0, symlink(outside_target, link_path));
|
||||
|
||||
rc = sched_validate_record_filename(record_dir, "looks_safe", out,
|
||||
sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT(-EACCES, rc);
|
||||
}
|
||||
|
||||
static void test_symlink_escape_not_caught_by_naive_textual_prefix_match(void)
|
||||
{
|
||||
/* The exact trap this task's security brief calls out by name:
|
||||
* record_dir="<tmp>/xce" vs a SIBLING directory
|
||||
* "<tmp>/xce-evil" -- the sibling's path textually STARTS WITH
|
||||
* record_dir's canonical path, so a naive strncmp()-only containment
|
||||
* check would wrongly accept it. A pre-existing symlink inside
|
||||
* record_dir points at a file inside that sibling; a correct
|
||||
* canonical-path comparison (requiring the byte after the prefix to
|
||||
* be '/') must still reject it. */
|
||||
char base[PATH_MAX];
|
||||
char record_dir[PATH_MAX];
|
||||
char sibling_dir[PATH_MAX];
|
||||
char sibling_target[PATH_MAX];
|
||||
char link_path[PATH_MAX];
|
||||
char out[PATH_MAX];
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(base, sizeof(base), "_prefixtrap");
|
||||
|
||||
snprintf(record_dir, sizeof(record_dir), "%s/xce", base);
|
||||
TEST_ASSERT_EQUAL_INT(0, mkdir(record_dir, 0755));
|
||||
|
||||
snprintf(sibling_dir, sizeof(sibling_dir), "%s/xce-evil", base);
|
||||
TEST_ASSERT_EQUAL_INT(0, mkdir(sibling_dir, 0755));
|
||||
|
||||
snprintf(sibling_target, sizeof(sibling_target), "%s/payload", sibling_dir);
|
||||
touch_file(sibling_target);
|
||||
|
||||
snprintf(link_path, sizeof(link_path), "%s/trap", record_dir);
|
||||
TEST_ASSERT_EQUAL_INT(0, symlink(sibling_target, link_path));
|
||||
|
||||
rc = sched_validate_record_filename(record_dir, "trap", out, sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT(-EACCES, rc);
|
||||
}
|
||||
|
||||
static void test_symlink_inside_record_dir_staying_contained_is_accepted(void)
|
||||
{
|
||||
/* Sanity companion to the two rejection tests above: a symlink whose
|
||||
* target genuinely stays within record_dir must be ACCEPTED -- proves
|
||||
* the containment check isn't simply "reject every symlink". */
|
||||
char record_dir[PATH_MAX];
|
||||
char resolved_dir[PATH_MAX];
|
||||
char real_target[PATH_MAX];
|
||||
char link_path[PATH_MAX];
|
||||
char out[PATH_MAX];
|
||||
char expected[PATH_MAX];
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_inbound");
|
||||
TEST_ASSERT_NOT_NULL(realpath(record_dir, resolved_dir));
|
||||
|
||||
snprintf(real_target, sizeof(real_target), "%s/real.replay", resolved_dir);
|
||||
touch_file(real_target);
|
||||
|
||||
snprintf(link_path, sizeof(link_path), "%s/alias", resolved_dir);
|
||||
TEST_ASSERT_EQUAL_INT(0, symlink(real_target, link_path));
|
||||
|
||||
rc = sched_validate_record_filename(record_dir, "alias", out, sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
|
||||
snprintf(expected, sizeof(expected), "%s/alias", resolved_dir);
|
||||
TEST_ASSERT_EQUAL_STRING(expected, out);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Hard-link inode-aliasing escape -- CWE-59 link-following via a hard */
|
||||
/* link rather than a symlink (Req 5 AC 10's core security property) */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_hardlink_inside_record_dir_aliasing_outside_is_rejected(void)
|
||||
{
|
||||
/* record_dir contains a REGULAR file (named exactly like a legitimate,
|
||||
* innocent-looking filename) that is actually a hard link to a victim
|
||||
* file OUTSIDE record_dir -- same inode, no separate "link target"
|
||||
* path for realpath()/lstat() to resolve and contain-check the way
|
||||
* there is for a symlink (mirrors
|
||||
* test_symlink_inside_record_dir_escaping_outside_is_rejected above,
|
||||
* substituting link() for symlink()). The filename component itself
|
||||
* violates neither the '/' nor the ".." rule, and the entry IS
|
||||
* S_ISREG and canonically inside record_dir -- only the st_nlink > 1
|
||||
* check can catch this. This exercises real link(2)/lstat(2) kernel
|
||||
* behavior (no mocking): a genuine hard link is created on disk and
|
||||
* genuinely shares the victim's inode, so if this test passes only
|
||||
* because the fix is in place, the hard link IS a real bypass on this
|
||||
* platform absent that fix (proven by the RED phase below). */
|
||||
char record_dir[PATH_MAX];
|
||||
char outside_dir[PATH_MAX];
|
||||
char victim[PATH_MAX];
|
||||
char aliased_path[PATH_MAX];
|
||||
char out[PATH_MAX];
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_hlsrc");
|
||||
make_tmp_dir(outside_dir, sizeof(outside_dir), "_hldst");
|
||||
|
||||
snprintf(victim, sizeof(victim), "%s/victim", outside_dir);
|
||||
touch_file(victim);
|
||||
|
||||
snprintf(aliased_path, sizeof(aliased_path), "%s/looks_safe", record_dir);
|
||||
TEST_ASSERT_EQUAL_INT(0, link(victim, aliased_path));
|
||||
|
||||
rc = sched_validate_record_filename(record_dir, "looks_safe", out,
|
||||
sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT(-EACCES, rc);
|
||||
}
|
||||
|
||||
static void test_symlink_to_hardlinked_alias_inside_record_dir_is_rejected(void)
|
||||
{
|
||||
/* One extra hop beyond both escape tests above: record_dir contains a
|
||||
* REGULAR file ("internal") that is a hard link to a victim file
|
||||
* OUTSIDE record_dir (mirrors
|
||||
* test_hardlink_inside_record_dir_aliasing_outside_is_rejected), PLUS a
|
||||
* SYMLINK ("alias") inside record_dir pointing at that hard-linked
|
||||
* "internal" entry (mirrors
|
||||
* test_symlink_inside_record_dir_escaping_outside_is_rejected's shape).
|
||||
* Requesting "internal" directly is already caught by the direct-entry
|
||||
* st_nlink > 1 check. Requesting "alias" must ALSO be rejected: the
|
||||
* S_ISLNK branch's realpath()/path_is_contained() call only confirms
|
||||
* the resolved target is textually/canonically inside record_dir --
|
||||
* "internal" IS inside record_dir, so containment alone is not enough.
|
||||
* The resolved target must ALSO be checked for st_nlink > 1, exactly
|
||||
* like the direct-entry branch does, or fopen()-ing the returned
|
||||
* out_path follows the symlink into the aliased outside inode and
|
||||
* corrupts the victim file -- same CWE-59 impact, reached via one
|
||||
* extra symlink hop. */
|
||||
char record_dir[PATH_MAX];
|
||||
char outside_dir[PATH_MAX];
|
||||
char victim[PATH_MAX];
|
||||
char internal_path[PATH_MAX];
|
||||
char alias_path[PATH_MAX];
|
||||
char out[PATH_MAX];
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_hlaliassrc");
|
||||
make_tmp_dir(outside_dir, sizeof(outside_dir), "_hlaliasdst");
|
||||
|
||||
snprintf(victim, sizeof(victim), "%s/victim", outside_dir);
|
||||
touch_file(victim);
|
||||
|
||||
snprintf(internal_path, sizeof(internal_path), "%s/internal", record_dir);
|
||||
TEST_ASSERT_EQUAL_INT(0, link(victim, internal_path));
|
||||
|
||||
snprintf(alias_path, sizeof(alias_path), "%s/alias", record_dir);
|
||||
TEST_ASSERT_EQUAL_INT(0, symlink(internal_path, alias_path));
|
||||
|
||||
rc = sched_validate_record_filename(record_dir, "alias", out, sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT(-EACCES, rc);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
/* Defensive: existing non-regular, non-symlink entry is rejected */
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
static void test_existing_directory_with_same_name_rejected(void)
|
||||
{
|
||||
char record_dir[PATH_MAX];
|
||||
char subdir[PATH_MAX];
|
||||
char out[PATH_MAX];
|
||||
int rc;
|
||||
|
||||
make_tmp_dir(record_dir, sizeof(record_dir), "_subdir");
|
||||
snprintf(subdir, sizeof(subdir), "%s/oops", record_dir);
|
||||
TEST_ASSERT_EQUAL_INT(0, mkdir(subdir, 0755));
|
||||
|
||||
rc = sched_validate_record_filename(record_dir, "oops", out, sizeof(out));
|
||||
TEST_ASSERT_EQUAL_INT(-EEXIST, rc);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
|
||||
RUN_TEST(test_null_record_dir_returns_einval);
|
||||
RUN_TEST(test_null_filename_returns_einval);
|
||||
RUN_TEST(test_null_out_path_returns_einval);
|
||||
RUN_TEST(test_zero_out_path_len_returns_einval);
|
||||
RUN_TEST(test_empty_filename_returns_einval);
|
||||
|
||||
RUN_TEST(test_filename_with_slash_rejected_before_filesystem_access);
|
||||
RUN_TEST(test_dotdot_all_positions_rejected_before_filesystem_access);
|
||||
|
||||
RUN_TEST(test_nonexistent_record_dir_with_valid_filename_returns_error);
|
||||
RUN_TEST(test_record_dir_that_is_a_file_returns_enotdir);
|
||||
|
||||
RUN_TEST(test_legitimate_bare_filename_resolves_inside_record_dir);
|
||||
RUN_TEST(test_legitimate_filename_overwriting_existing_regular_file_ok);
|
||||
RUN_TEST(test_out_path_buffer_too_small_returns_enametoolong);
|
||||
|
||||
RUN_TEST(test_symlink_inside_record_dir_escaping_outside_is_rejected);
|
||||
RUN_TEST(test_symlink_escape_not_caught_by_naive_textual_prefix_match);
|
||||
RUN_TEST(test_symlink_inside_record_dir_staying_contained_is_accepted);
|
||||
|
||||
RUN_TEST(test_hardlink_inside_record_dir_aliasing_outside_is_rejected);
|
||||
RUN_TEST(test_symlink_to_hardlinked_alias_inside_record_dir_is_rejected);
|
||||
|
||||
RUN_TEST(test_existing_directory_with_same_name_rejected);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue