From 76ba7aae6a507a76b88473eb1943e51fa630d012 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 24 Oct 2026 14:45:32 +0200 Subject: [PATCH] Cover encrypted file aliases and I/O failures through publication --- tests/Makefile.am | 5 + tests/test_encrypted_file.c | 188 ++++++++++++++++++++++++++++++++++-- 2 files changed, 184 insertions(+), 9 deletions(-) diff --git a/tests/Makefile.am b/tests/Makefile.am index b5608f8e..902c1ad9 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -389,6 +389,11 @@ test_attachment_SOURCES = test_attachment.c test_attachment_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) test_encrypted_file_SOURCES = test_encrypted_file.c test_encrypted_file_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) +if OS_LINUX +test_encrypted_file_LDFLAGS = -Wl,--wrap=fopen -Wl,--wrap=fdopen -Wl,--wrap=fread -Wl,--wrap=__fread_chk \ + -Wl,--wrap=fwrite -Wl,--wrap=ferror -Wl,--wrap=fflush -Wl,--wrap=fclose -Wl,--wrap=fseeko \ + -Wl,--wrap=fsync -Wl,--wrap=rename -Wl,--wrap=random_bytes +endif test_encrypted_attachment_SOURCES = test_encrypted_attachment.c $(test_clock_sources) test_encrypted_attachment_CFLAGS = $(test_dm_e2e_CFLAGS) test_encrypted_attachment_LDADD = $(LIBUTUN) $(CRYPTO_LIBS) $(COMMON_LIBS) diff --git a/tests/test_encrypted_file.c b/tests/test_encrypted_file.c index 66c153e8..48613d9a 100644 --- a/tests/test_encrypted_file.c +++ b/tests/test_encrypted_file.c @@ -6,6 +6,75 @@ #include "test_utils.h" #include #include +#include +#ifdef __linux__ +#include + +enum fault { F_NONE, F_OPEN, F_FDOPEN, F_READ, F_WRITE, F_SEEK, F_FLUSH, F_INPUT_CLOSE, F_OUTPUT_CLOSE, + F_SYNC, F_RENAME, F_DIRECTORY, F_RANDOM }; +static enum fault fault; +static int injected, fault_after, read_failed; +static FILE *input_file, *output_file; +FILE* __real_fopen(const char*, const char*); +FILE* __real_fdopen(int, const char*); +size_t __real_fread(void*, size_t, size_t, FILE*), __real_fwrite(const void*, size_t, size_t, FILE*); +size_t __real___fread_chk(void*, size_t, size_t, size_t, FILE*); +int __real_ferror(FILE*), __real_fflush(FILE*), __real_fclose(FILE*), __real_fsync(int); +int __real_fseeko(FILE*, off_t, int), __real_rename(const char*, const char*); +int __real_random_bytes(uint8_t*, size_t); +static int inject(enum fault value) { + if (fault != value || injected || --fault_after > 0) return 0; + injected = 1; errno = EIO; return 1; +} +FILE* __wrap_fopen(const char* path, const char* mode) { + if (!strcmp(mode, "rb") && inject(F_OPEN)) return NULL; + FILE* file = __real_fopen(path, mode); + if (!strcmp(mode, "rb")) input_file = file; + return file; +} +FILE* __wrap_fdopen(int fd, const char* mode) { + if (inject(F_FDOPEN)) return NULL; + return output_file = __real_fdopen(fd, mode); +} +size_t __wrap_fread(void* data, size_t size, size_t count, FILE* file) { + if (file == input_file && inject(F_READ)) { read_failed = 1; return 0; } + return __real_fread(data, size, count, file); +} +size_t __wrap___fread_chk(void* data, size_t cap, size_t size, size_t count, FILE* file) { + if (file == input_file && inject(F_READ)) { read_failed = 1; return 0; } + return __real___fread_chk(data, cap, size, count, file); +} +int __wrap_ferror(FILE* file) { return file == input_file && read_failed ? 1 : __real_ferror(file); } +size_t __wrap_fwrite(const void* data, size_t size, size_t count, FILE* file) { + return file == output_file && inject(F_WRITE) ? 0 : __real_fwrite(data, size, count, file); +} +int __wrap_fflush(FILE* file) { return file == output_file && inject(F_FLUSH) ? EOF : __real_fflush(file); } +int __wrap_fseeko(FILE* file, off_t offset, int whence) { return inject(F_SEEK) ? -1 : __real_fseeko(file, offset, whence); } +int __wrap_fclose(FILE* file) { + int error = (file == input_file && inject(F_INPUT_CLOSE)) || (file == output_file && inject(F_OUTPUT_CLOSE)); + if (file == input_file) { input_file = NULL; read_failed = 0; } + if (file == output_file) output_file = NULL; + int result = __real_fclose(file); + if (error) { errno = EIO; return EOF; } + return result; +} +int __wrap_fsync(int fd) { + struct stat data; assert(!fstat(fd, &data)); + return inject(S_ISDIR(data.st_mode) ? F_DIRECTORY : F_SYNC) ? -1 : __real_fsync(fd); +} +int __wrap_rename(const char* from, const char* to) { return inject(F_RENAME) ? -1 : __real_rename(from, to); } +int __wrap_random_bytes(uint8_t* data, size_t len) { return inject(F_RANDOM) ? -1 : __real_random_bytes(data, len); } +static unsigned count_fds(void) { + DIR* dir = opendir("/proc/self/fd"); assert(dir); + unsigned count = 0; + while (readdir(dir)) count++; + assert(!closedir(dir)); return count; +} +#endif + +static void hash_same(const char* path, const uint8_t expected[32]) { + uint8_t actual[32]; assert(!ma_sha256_file(path, actual) && !memcmp(actual, expected, 32)); +} static void roundtrip(const char* dir, unsigned size) { char source[768], cipher[768], output[768]; @@ -17,37 +86,138 @@ static void roundtrip(const char* dir, unsigned size) { uint8_t key[32], wrong[32], id[16], hash[32]; assert(RAND_bytes(key, 32) == 1 && RAND_bytes(wrong, 32) == 1 && RAND_bytes(id, 16) == 1); struct attachment_info info = { .kind = ATTACHMENT_FILE, .name = "секретный-документ.bin" }, result; + struct file_io_temp temporary = {0}; char type[32]; - assert(!encrypted_file_write(source, cipher, key, 42, id, &info, "application/octet-stream")); + assert(!encrypted_file_write(source, cipher, &temporary, key, 42, id, &info, "application/octet-stream")); + assert(file_io_remove_orphan(temporary.temporary) == 0); + assert(!file_io_temp_publish(&temporary, source, cipher)); file_io_temp_discard(&temporary); assert(!ma_sha256_file(cipher, hash)); - assert(!encrypted_file_read(cipher, output, key, 42, id, hash, &result, type)); + assert(!encrypted_file_read(cipher, output, &temporary, key, 42, id, hash, &result, type)); + assert(ma_file_size(output) < 0 && file_io_remove_orphan(temporary.temporary) == 0); + assert(!file_io_temp_publish(&temporary, cipher, output)); file_io_temp_discard(&temporary); assert(!strcmp(info.name, result.name) && !strcmp(type, "application/octet-stream")); f = fopen(output, "rb"); assert(f); for (unsigned i = 0; i < size; i++) assert(fgetc(f) == (int)((i * 73 + 19) & 255)); assert(fgetc(f) == EOF); assert(!fclose(f)); assert(!remove(output)); - assert(encrypted_file_read(cipher, output, wrong, 42, id, hash, &result, type) < 0); + assert(encrypted_file_read(cipher, output, &temporary, wrong, 42, id, hash, &result, type) < 0); assert(ma_file_size(output) < 0); - assert(encrypted_file_read(cipher, output, key, 43, id, hash, &result, type) < 0); + assert(encrypted_file_read(cipher, output, &temporary, key, 43, id, hash, &result, type) < 0); id[0] ^= 1; - assert(encrypted_file_read(cipher, output, key, 42, id, hash, &result, type) < 0); id[0] ^= 1; + assert(encrypted_file_read(cipher, output, &temporary, key, 42, id, hash, &result, type) < 0); id[0] ^= 1; /* Подмена metadata даже с заново вычисленным внешним hash не проходит CCM. */ f = fopen(cipher, "rb+"); assert(f && !fseek(f, 70, SEEK_SET)); int c = fgetc(f); assert(c != EOF && !fseek(f, 70, SEEK_SET) && fputc(c ^ 1, f) != EOF && !fclose(f)); - assert(encrypted_file_read(cipher, output, key, 42, id, hash, &result, type) < 0); + assert(encrypted_file_read(cipher, output, &temporary, key, 42, id, hash, &result, type) < 0); assert(!ma_sha256_file(cipher, hash)); - assert(encrypted_file_read(cipher, output, key, 42, id, hash, &result, type) < 0 && ma_file_size(output) < 0); - assert(!encrypted_file_write(source, cipher, key, 42, id, &info, "application/octet-stream")); + assert(encrypted_file_read(cipher, output, &temporary, key, 42, id, hash, &result, type) < 0 && ma_file_size(output) < 0); + assert(!encrypted_file_write(source, cipher, &temporary, key, 42, id, &info, "application/octet-stream")); + assert(!file_io_temp_publish(&temporary, source, cipher)); file_io_temp_discard(&temporary); /* Обрезанный контейнер и лишние байты отвергаются независимо от manifest hash. */ f = fopen(cipher, "ab"); assert(f && fputc(0, f) != EOF && !fclose(f)); assert(!ma_sha256_file(cipher, hash)); - assert(encrypted_file_read(cipher, output, key, 42, id, hash, &result, type) < 0 && ma_file_size(output) < 0); + assert(encrypted_file_read(cipher, output, &temporary, key, 42, id, hash, &result, type) < 0 && ma_file_size(output) < 0); printf("PASS: encrypted container size=%u, metadata, identity, key and integrity checks\n", size); } +/* Оба API отвергают разные имена одного файла, не меняя содержимое и не удаляя alias. */ +static void aliases(const char* dir) { + char source[768], cipher[768], target[768]; + snprintf(source, sizeof(source), "%s/source", dir); snprintf(cipher, sizeof(cipher), "%s/cipher", dir); + uint8_t key[32] = {1}, id[16] = {2}, hash[32], before[32]; + struct attachment_info info = {.kind = ATTACHMENT_FILE, .name = "example.bin"}, result; + struct file_io_temp temporary = {0}; + char type[32]; + assert(!encrypted_file_write(source, cipher, &temporary, key, 42, id, &info, "application/octet-stream")); + assert(!file_io_temp_publish(&temporary, source, cipher)); + for (int decrypt = 0; decrypt < 2; decrypt++) { + const char* input = decrypt ? cipher : source; + assert(!ma_sha256_file(input, before)); + for (int kind = 0; kind < 4; kind++) { + if (!kind) snprintf(target, sizeof(target), "%s", input); + else if (kind == 1) snprintf(target, sizeof(target), "%s/./%s", dir, decrypt ? "cipher" : "source"); + else { +#ifndef _WIN32 + snprintf(target, sizeof(target), "%s/alias", dir); + assert(kind == 2 ? !link(input, target) : !symlink(input, target)); +#else + continue; +#endif + } + int error = decrypt ? encrypted_file_read(input, target, &temporary, key, 42, id, before, &result, type) + : encrypted_file_write(input, target, &temporary, key, 42, id, &info, "application/octet-stream"); + assert(error < 0 && !temporary.file && !temporary.temporary[0]); + hash_same(input, before); hash_same(target, before); + if (kind >= 2) assert(!remove(target)); + } + } + /* Активный output чужой операции не теряется даже при ошибочных аргументах. */ + snprintf(target, sizeof(target), "%s/output", dir); + assert(!file_io_temp_open(&temporary, target, 0)); + assert(encrypted_file_write(source, target, &temporary, key, 42, id, &info, "application/octet-stream") < 0); + assert(encrypted_file_read(cipher, target, &temporary, key, 42, id, before, &result, type) < 0); + assert(temporary.file && temporary.temporary[0] && file_io_remove_orphan(temporary.temporary) == 0); + file_io_temp_discard(&temporary); + /* Между подготовкой и publish цель стала alias исходника: второй контроль сохраняет его. */ +#ifndef _WIN32 + assert(!ma_sha256_file(source, before)); + assert(!encrypted_file_write(source, target, &temporary, key, 42, id, &info, "application/octet-stream")); + assert(!link(source, target)); + assert(file_io_temp_publish(&temporary, source, target) < 0 && temporary.temporary[0]); + file_io_temp_discard(&temporary); hash_same(source, before); hash_same(target, before); assert(!remove(target)); +#endif + assert(!ma_sha256_file(cipher, hash)); + printf("PASS: encrypt/decrypt aliases, active output and alias before publication\n"); +} + +#ifdef __linux__ +static void temp_count(void* arg, const char* path, int64_t size) { + (void)size; if (strstr(path, ".tmp.")) (*(unsigned*)arg)++; +} +/* Реальные файловые операции и CCM; каждый отказ сохраняет source и прежний target до rename. */ +static void failures(const char* dir) { + char source[768], cipher[768], target[768]; + snprintf(source, sizeof(source), "%s/source", dir); snprintf(cipher, sizeof(cipher), "%s/cipher", dir); + snprintf(target, sizeof(target), "%s/target", dir); + uint8_t key[32] = {1}, id[16] = {2}, hash[32], before[32], previous[32]; + struct attachment_info info = {.kind = ATTACHMENT_FILE, .name = "example.bin"}; + struct file_io_temp output = {0}; + assert(!ma_sha256_file(cipher, hash)); + for (int decrypt = 0; decrypt < 2; decrypt++) for (enum fault test = F_OPEN; test <= F_RANDOM; test++) { + if (decrypt && test == F_SEEK) continue; + const char* input = decrypt ? cipher : source; + assert(!ma_sha256_file(input, before)); + FILE* file = fopen(target, "wb"); assert(file && fputs("previous", file) >= 0 && !fclose(file)); + assert(!ma_sha256_file(target, previous)); + struct attachment_info result, unchanged; memset(&result, 0xa5, sizeof(result)); unchanged = result; + char type[32], old_type[32]; memset(type, 0xa5, sizeof(type)); memcpy(old_type, type, sizeof(type)); + unsigned fds = count_fds(); + fault = test; injected = 0; fault_after = decrypt && test == F_INPUT_CLOSE ? 2 : 1; + int error = decrypt ? encrypted_file_read(input, target, &output, key, 42, id, hash, &result, type) + : encrypted_file_write(input, target, &output, key, 42, id, &info, "application/octet-stream"); + int prepared = !error; + if (prepared) error = file_io_temp_publish(&output, input, target); + fault = F_NONE; + assert(error < 0 && injected); + if (decrypt && !prepared) assert(!memcmp(&result, &unchanged, sizeof(result)) && !memcmp(type, old_type, sizeof(type))); + if (test == F_DIRECTORY) assert(!output.temporary[0]); + file_io_temp_discard(&output); + assert(count_fds() == fds); hash_same(input, before); + if (test != F_DIRECTORY) hash_same(target, previous); + unsigned temps = 0; assert(!ma_dir_walk(dir, temp_count, &temps) && !temps); + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS encrypted I/O failure decrypt=%d phase=%d installed=%d", decrypt, test, test == F_DIRECTORY); + } + assert(!remove(target)); +} +#endif + int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); char dir[512] = "/tmp/utun_encrypted_file_XXXXXX"; assert(!test_mkdtemp(dir)); const unsigned sizes[] = { 0, 1, 32767, 32768, 32769, 98321 }; for (unsigned i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) roundtrip(dir, sizes[i]); + aliases(dir); +#ifdef __linux__ + failures(dir); +#endif return 0; }