security: close filesystem race findings
This commit is contained in:
parent
8cca841258
commit
d8668e6d64
10 changed files with 698 additions and 133 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -121,6 +121,8 @@ jobs:
|
|||
run: make CC=${{ matrix.cc }} V=1 WITH_SDK=0 WITH_PQBOX=0 check
|
||||
- name: Extended upstream tests
|
||||
run: make CC=${{ matrix.cc }} V=1 WITH_SDK=0 WITH_PQBOX=0 test-all
|
||||
- name: In-tree SDK atomic key-save regression
|
||||
run: make CC=${{ matrix.cc }} V=1 sdk-test
|
||||
- name: Functional test of the built CLI
|
||||
run: bash scripts/test-installed-zupt.sh "$PWD/zupt"
|
||||
|
||||
|
|
|
|||
1
Makefile
1
Makefile
|
|
@ -540,6 +540,7 @@ test-all: check
|
|||
# Release-only gates need a committed Git checkout and packaging metadata.
|
||||
# Keep them out of downstream %check, which intentionally has no dist rebuild.
|
||||
release-check: test-all audit-licenses
|
||||
$(Q)$(MAKE) sdk-test
|
||||
$(Q)bash tests/test_static_analysis.sh
|
||||
$(Q)bash tests/test_packaging_syntax.sh
|
||||
$(Q)bash scripts/test-installed-zupt.sh ./$(TARGET)
|
||||
|
|
|
|||
|
|
@ -556,20 +556,46 @@ void zuptsdk_keypair_destroy(zuptsdk_keypair_t *kp) {
|
|||
static int zsdk_copy_file(const char *src, const char *dst, mode_t mode) {
|
||||
FILE *fi = fopen(src, "rb");
|
||||
if (!fi) return ZSDK_FAIL(ZUPTSDK_ERR_IO, "open %s", src);
|
||||
FILE *fo = fopen(dst, "wb");
|
||||
if (!fo) { fclose(fi); return ZSDK_FAIL(ZUPTSDK_ERR_IO, "create %s", dst); }
|
||||
uint8_t buf[4096];
|
||||
size_t n;
|
||||
int rc = ZUPTSDK_OK;
|
||||
while ((n = fread(buf, 1, sizeof(buf), fi)) > 0)
|
||||
if (fwrite(buf, 1, n, fo) != n) { rc = ZSDK_FAIL(ZUPTSDK_ERR_IO, "write %s", dst); break; }
|
||||
zuptsdk_secure_zero(buf, sizeof(buf));
|
||||
fclose(fi); fclose(fo);
|
||||
FILE *fo = NULL;
|
||||
zupt_atomic_output_t *output = zupt_atomic_output_open(dst, &fo);
|
||||
if (!output) {
|
||||
int saved_errno = errno;
|
||||
fclose(fi);
|
||||
errno = saved_errno;
|
||||
return ZSDK_FAIL(ZUPTSDK_ERR_IO, "create %s", dst);
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
if (rc == ZUPTSDK_OK) chmod(dst, mode);
|
||||
/* Apply permissions to the private temporary object, never to a
|
||||
* re-resolved destination path. */
|
||||
if (fchmod(fileno(fo), mode) != 0) {
|
||||
int saved_errno = errno;
|
||||
fclose(fi);
|
||||
(void)zupt_atomic_output_finish(output, 0);
|
||||
errno = saved_errno;
|
||||
return ZSDK_FAIL(ZUPTSDK_ERR_IO, "set permissions on %s", dst);
|
||||
}
|
||||
#else
|
||||
(void)mode;
|
||||
#endif
|
||||
|
||||
uint8_t buf[4096];
|
||||
size_t n;
|
||||
int rc = ZUPTSDK_OK;
|
||||
while ((n = fread(buf, 1, sizeof(buf), fi)) > 0) {
|
||||
if (fwrite(buf, 1, n, fo) != n) {
|
||||
rc = ZSDK_FAIL(ZUPTSDK_ERR_IO, "write %s", dst);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rc == ZUPTSDK_OK && ferror(fi))
|
||||
rc = ZSDK_FAIL(ZUPTSDK_ERR_IO, "read %s", src);
|
||||
zuptsdk_secure_zero(buf, sizeof(buf));
|
||||
if (fclose(fi) != 0 && rc == ZUPTSDK_OK)
|
||||
rc = ZSDK_FAIL(ZUPTSDK_ERR_IO, "close %s", src);
|
||||
if (zupt_atomic_output_finish(output, rc == ZUPTSDK_OK) != 0 &&
|
||||
rc == ZUPTSDK_OK)
|
||||
rc = ZSDK_FAIL(ZUPTSDK_ERR_IO, "publish %s", dst);
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#ifndef _WIN32
|
||||
#include <sys/stat.h>
|
||||
#endif
|
||||
#include <unistd.h>
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
|
|
@ -47,6 +50,79 @@ static const uint8_t TEST_DATA[] =
|
|||
"Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod. "
|
||||
"End of test data.\n";
|
||||
|
||||
#ifndef _WIN32
|
||||
static int file_matches(const char *path, const void *expected,
|
||||
size_t expected_size) {
|
||||
struct stat info;
|
||||
char observed[128];
|
||||
if (expected_size > sizeof(observed) || stat(path, &info) != 0 ||
|
||||
info.st_size < 0 || (uint64_t)info.st_size != (uint64_t)expected_size)
|
||||
return 0;
|
||||
FILE *stream = fopen(path, "rb");
|
||||
if (!stream) return 0;
|
||||
size_t got = fread(observed, 1, expected_size, stream);
|
||||
int read_error = ferror(stream);
|
||||
int close_rc = fclose(stream);
|
||||
return got == expected_size && !read_error && close_rc == 0 &&
|
||||
memcmp(observed, expected, expected_size) == 0;
|
||||
}
|
||||
|
||||
static int private_key_save_avoids_link_targets(const zuptsdk_keypair_t *kp) {
|
||||
static const char sentinel[] = "do not replace through a symlink\n";
|
||||
char workspace[] = "/tmp/zupt-sdk-link-save.XXXXXX";
|
||||
char target[192];
|
||||
char symlink_path[192];
|
||||
char hardlink_path[192];
|
||||
FILE *stream;
|
||||
struct stat target_st;
|
||||
struct stat output_st;
|
||||
int ok = 0;
|
||||
|
||||
if (!mkdtemp(workspace)) return 0;
|
||||
snprintf(target, sizeof(target), "%s/target", workspace);
|
||||
snprintf(symlink_path, sizeof(symlink_path), "%s/symlink-output",
|
||||
workspace);
|
||||
snprintf(hardlink_path, sizeof(hardlink_path), "%s/hardlink-output",
|
||||
workspace);
|
||||
|
||||
stream = fopen(target, "wb");
|
||||
if (!stream) goto cleanup;
|
||||
size_t written = fwrite(sentinel, 1, sizeof(sentinel) - 1, stream);
|
||||
int close_rc = fclose(stream);
|
||||
if (written != sizeof(sentinel) - 1 || close_rc != 0)
|
||||
goto cleanup;
|
||||
|
||||
if (symlink(target, symlink_path) != 0 ||
|
||||
zuptsdk_keypair_save_private(kp, symlink_path) != ZUPTSDK_OK ||
|
||||
!file_matches(target, sentinel, sizeof(sentinel) - 1) ||
|
||||
stat(target, &target_st) != 0 || lstat(symlink_path, &output_st) != 0 ||
|
||||
(target_st.st_dev == output_st.st_dev &&
|
||||
target_st.st_ino == output_st.st_ino) ||
|
||||
!S_ISREG(output_st.st_mode) || output_st.st_size <= 0 ||
|
||||
(output_st.st_mode & 0777) != 0600)
|
||||
goto cleanup;
|
||||
|
||||
if (link(target, hardlink_path) != 0 ||
|
||||
zuptsdk_keypair_save_private(kp, hardlink_path) != ZUPTSDK_OK ||
|
||||
!file_matches(target, sentinel, sizeof(sentinel) - 1) ||
|
||||
stat(target, &target_st) != 0 || stat(hardlink_path, &output_st) != 0 ||
|
||||
(target_st.st_dev == output_st.st_dev &&
|
||||
target_st.st_ino == output_st.st_ino) ||
|
||||
!S_ISREG(output_st.st_mode) || output_st.st_size <= 0 ||
|
||||
(output_st.st_mode & 0777) != 0600)
|
||||
goto cleanup;
|
||||
|
||||
ok = 1;
|
||||
|
||||
cleanup:
|
||||
unlink(symlink_path);
|
||||
unlink(hardlink_path);
|
||||
unlink(target);
|
||||
rmdir(workspace);
|
||||
return ok;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void test_version(void) {
|
||||
TEST("version_string returns non-NULL");
|
||||
const char *v = zuptsdk_version_string();
|
||||
|
|
@ -250,6 +326,15 @@ cleanup:
|
|||
|
||||
static void test_keypair_pq(void) {
|
||||
TEST("keypair_generate + compress_pq + extract_pq");
|
||||
char saved_priv[160];
|
||||
char saved_pub[160];
|
||||
snprintf(saved_priv, sizeof(saved_priv), "/tmp/_zsdk_priv_%ld.key",
|
||||
(long)getpid());
|
||||
snprintf(saved_pub, sizeof(saved_pub), "/tmp/_zsdk_pub_%ld.key",
|
||||
(long)getpid());
|
||||
unlink(saved_priv);
|
||||
unlink(saved_pub);
|
||||
|
||||
zuptsdk_ctx_t *ctx = NULL;
|
||||
CHECK(zuptsdk_ctx_create(&ctx), "ctx");
|
||||
|
||||
|
|
@ -257,17 +342,35 @@ static void test_keypair_pq(void) {
|
|||
int rc = zuptsdk_keypair_generate(ctx, &kp);
|
||||
if (rc != ZUPTSDK_OK) { FAIL("keygen"); zuptsdk_ctx_destroy(ctx); return; }
|
||||
|
||||
#ifndef _WIN32
|
||||
if (!private_key_save_avoids_link_targets(kp)) {
|
||||
FAIL("private key save followed a symlink or hardlink target");
|
||||
goto err;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Save and load to exercise that path too */
|
||||
rc = zuptsdk_keypair_save_private(kp, "/tmp/_zsdk_priv.key");
|
||||
rc = zuptsdk_keypair_save_private(kp, saved_priv);
|
||||
if (rc != ZUPTSDK_OK) { FAIL("save priv"); goto err; }
|
||||
rc = zuptsdk_keypair_save_public(kp, "/tmp/_zsdk_pub.key");
|
||||
rc = zuptsdk_keypair_save_public(kp, saved_pub);
|
||||
if (rc != ZUPTSDK_OK) { FAIL("save pub"); goto err; }
|
||||
#ifndef _WIN32
|
||||
struct stat private_st;
|
||||
struct stat public_st;
|
||||
if (stat(saved_priv, &private_st) != 0 ||
|
||||
stat(saved_pub, &public_st) != 0 ||
|
||||
(private_st.st_mode & 0777) != 0600 ||
|
||||
(public_st.st_mode & 0777) != 0644) {
|
||||
FAIL("saved key permissions do not match the requested modes");
|
||||
goto err;
|
||||
}
|
||||
#endif
|
||||
|
||||
zuptsdk_pubkey_t *pub = NULL;
|
||||
zuptsdk_privkey_t *priv = NULL;
|
||||
rc = zuptsdk_pubkey_load("/tmp/_zsdk_pub.key", &pub);
|
||||
rc = zuptsdk_pubkey_load(saved_pub, &pub);
|
||||
if (rc != ZUPTSDK_OK) { FAIL("load pub"); goto err; }
|
||||
rc = zuptsdk_privkey_load("/tmp/_zsdk_priv.key", &priv);
|
||||
rc = zuptsdk_privkey_load(saved_priv, &priv);
|
||||
if (rc != ZUPTSDK_OK) { FAIL("load priv"); zuptsdk_pubkey_destroy(pub); goto err; }
|
||||
|
||||
zuptsdk_options_t *opts = NULL;
|
||||
|
|
@ -295,8 +398,8 @@ static void test_keypair_pq(void) {
|
|||
zuptsdk_privkey_destroy(priv);
|
||||
zuptsdk_options_destroy(opts);
|
||||
|
||||
unlink("/tmp/_zsdk_priv.key");
|
||||
unlink("/tmp/_zsdk_pub.key");
|
||||
unlink(saved_priv);
|
||||
unlink(saved_pub);
|
||||
|
||||
if (!ok) { FAIL("byte mismatch or rc != OK"); zuptsdk_keypair_destroy(kp); zuptsdk_ctx_destroy(ctx); return; }
|
||||
zuptsdk_keypair_destroy(kp);
|
||||
|
|
@ -305,6 +408,8 @@ static void test_keypair_pq(void) {
|
|||
return;
|
||||
|
||||
err:
|
||||
unlink(saved_priv);
|
||||
unlink(saved_pub);
|
||||
zuptsdk_keypair_destroy(kp);
|
||||
zuptsdk_ctx_destroy(ctx);
|
||||
}
|
||||
|
|
|
|||
123
src/zupt_disk.c
123
src/zupt_disk.c
|
|
@ -1220,23 +1220,35 @@ zupt_error_t zupt_disk_restore(const char *archive_path, const char *target_path
|
|||
#else
|
||||
int tgt_fd = -1;
|
||||
int is_block_dev = 0;
|
||||
struct stat target_st;
|
||||
|
||||
if (lstat(target_path, &target_st) == 0) {
|
||||
if (S_ISLNK(target_st.st_mode)) {
|
||||
fprintf(stderr, "Error: refusing a symbolic-link restore target.\n");
|
||||
fclose(f);
|
||||
return ZUPT_ERR_INVALID;
|
||||
}
|
||||
if (S_ISREG(target_st.st_mode)) {
|
||||
if (target_st.st_dev == archive_identity.device &&
|
||||
target_st.st_ino == archive_identity.inode) {
|
||||
/* Resolve the target exactly once before making any type or identity
|
||||
* decision. The open is non-truncating, O_NOFOLLOW rejects a final
|
||||
* symlink, and fstat classifies the kernel object that was actually
|
||||
* opened. Device restores retain this same descriptor through the final
|
||||
* write, so a concurrent pathname exchange cannot redirect the restore. */
|
||||
tgt_fd = open(target_path, O_WRONLY | O_NOFOLLOW | O_CLOEXEC |
|
||||
O_NONBLOCK | O_SYNC);
|
||||
if (tgt_fd >= 0) {
|
||||
struct stat opened_st;
|
||||
if (fstat(tgt_fd, &opened_st) != 0) {
|
||||
int saved_errno = errno;
|
||||
close(tgt_fd);
|
||||
tgt_fd = -1;
|
||||
errno = saved_errno;
|
||||
} else if (S_ISREG(opened_st.st_mode)) {
|
||||
int close_result = close(tgt_fd);
|
||||
tgt_fd = -1;
|
||||
if (close_result != 0) {
|
||||
fclose(f);
|
||||
return ZUPT_ERR_IO;
|
||||
}
|
||||
if (opened_st.st_dev == archive_identity.device &&
|
||||
opened_st.st_ino == archive_identity.inode) {
|
||||
fprintf(stderr,
|
||||
"Error: archive and restore target are the same file.\n");
|
||||
fclose(f);
|
||||
return ZUPT_ERR_INVALID;
|
||||
}
|
||||
if (target_st.st_nlink != 1) {
|
||||
if (opened_st.st_nlink != 1) {
|
||||
fprintf(stderr,
|
||||
"Error: refusing a multiply-linked restore target.\n");
|
||||
fclose(f);
|
||||
|
|
@ -1244,59 +1256,46 @@ zupt_error_t zupt_disk_restore(const char *archive_path, const char *target_path
|
|||
}
|
||||
target_atomic =
|
||||
zupt_atomic_output_open(target_path, &target_stream);
|
||||
} else if (S_ISBLK(target_st.st_mode) ||
|
||||
S_ISCHR(target_st.st_mode)) {
|
||||
tgt_fd = open(target_path, O_WRONLY | O_NOFOLLOW | O_CLOEXEC |
|
||||
O_NONBLOCK | O_SYNC);
|
||||
if (tgt_fd >= 0) {
|
||||
struct stat opened_st;
|
||||
if (fstat(tgt_fd, &opened_st) != 0 ||
|
||||
opened_st.st_dev != target_st.st_dev ||
|
||||
opened_st.st_ino != target_st.st_ino ||
|
||||
!(S_ISBLK(opened_st.st_mode) ||
|
||||
S_ISCHR(opened_st.st_mode))) {
|
||||
} else if (S_ISBLK(opened_st.st_mode) ||
|
||||
S_ISCHR(opened_st.st_mode)) {
|
||||
int flags = fcntl(tgt_fd, F_GETFL);
|
||||
if (flags < 0 ||
|
||||
fcntl(tgt_fd, F_SETFL, flags & ~O_NONBLOCK) != 0) {
|
||||
close(tgt_fd);
|
||||
tgt_fd = -1;
|
||||
} else {
|
||||
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
|
||||
uint64_t target_capacity = 0;
|
||||
if (!disk_restore_target_capacity(
|
||||
tgt_fd, &opened_st, &target_capacity)) {
|
||||
fprintf(stderr,
|
||||
"Error: cannot determine restore device "
|
||||
"capacity safely.\n");
|
||||
close(tgt_fd);
|
||||
tgt_fd = -1;
|
||||
errno = EAGAIN;
|
||||
} else if (expected_size > target_capacity) {
|
||||
fprintf(stderr,
|
||||
"Error: disk image (%llu bytes) exceeds "
|
||||
"restore device capacity (%llu bytes).\n",
|
||||
(unsigned long long)expected_size,
|
||||
(unsigned long long)target_capacity);
|
||||
close(tgt_fd);
|
||||
tgt_fd = -1;
|
||||
errno = EFBIG;
|
||||
} else {
|
||||
int flags = fcntl(tgt_fd, F_GETFL);
|
||||
if (flags < 0 ||
|
||||
fcntl(tgt_fd, F_SETFL, flags & ~O_NONBLOCK) != 0) {
|
||||
close(tgt_fd);
|
||||
tgt_fd = -1;
|
||||
} else {
|
||||
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
|
||||
uint64_t target_capacity = 0;
|
||||
if (!disk_restore_target_capacity(
|
||||
tgt_fd, &opened_st, &target_capacity)) {
|
||||
fprintf(stderr,
|
||||
"Error: cannot determine restore device "
|
||||
"capacity safely.\n");
|
||||
close(tgt_fd);
|
||||
tgt_fd = -1;
|
||||
} else if (expected_size > target_capacity) {
|
||||
fprintf(stderr,
|
||||
"Error: disk image (%llu bytes) exceeds "
|
||||
"restore device capacity (%llu bytes).\n",
|
||||
(unsigned long long)expected_size,
|
||||
(unsigned long long)target_capacity);
|
||||
close(tgt_fd);
|
||||
tgt_fd = -1;
|
||||
errno = EFBIG;
|
||||
} else {
|
||||
is_block_dev = 1;
|
||||
}
|
||||
#else
|
||||
fprintf(stderr,
|
||||
"Error: restore-device capacity queries are "
|
||||
"not supported on this platform.\n");
|
||||
close(tgt_fd);
|
||||
tgt_fd = -1;
|
||||
#endif
|
||||
}
|
||||
is_block_dev = 1;
|
||||
}
|
||||
#else
|
||||
fprintf(stderr,
|
||||
"Error: restore-device capacity queries are "
|
||||
"not supported on this platform.\n");
|
||||
close(tgt_fd);
|
||||
tgt_fd = -1;
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
close(tgt_fd);
|
||||
tgt_fd = -1;
|
||||
fprintf(stderr,
|
||||
"Error: restore target is not a regular file or device.\n");
|
||||
fclose(f);
|
||||
|
|
@ -1304,8 +1303,12 @@ zupt_error_t zupt_disk_restore(const char *archive_path, const char *target_path
|
|||
}
|
||||
} else if (errno == ENOENT) {
|
||||
target_atomic = zupt_atomic_output_open(target_path, &target_stream);
|
||||
} else if (errno == ELOOP) {
|
||||
fprintf(stderr, "Error: refusing a symbolic-link restore target.\n");
|
||||
fclose(f);
|
||||
return ZUPT_ERR_INVALID;
|
||||
} else {
|
||||
fprintf(stderr, "Error: Cannot inspect target '%s': %s\n",
|
||||
fprintf(stderr, "Error: Cannot open target '%s': %s\n",
|
||||
target_path, strerror(errno));
|
||||
fclose(f);
|
||||
return ZUPT_ERR_IO;
|
||||
|
|
|
|||
397
src/zupt_main.c
397
src/zupt_main.c
|
|
@ -25,7 +25,9 @@
|
|||
#ifdef _WIN32
|
||||
#include <conio.h>
|
||||
#include <windows.h>
|
||||
#include <winternl.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <termios.h>
|
||||
#endif
|
||||
|
|
@ -100,9 +102,11 @@ static int zupt_create_private_temp_directory(char *output, size_t capacity) {
|
|||
}
|
||||
return 0;
|
||||
#else
|
||||
static const char pattern[] = "/tmp/zupt-bench-XXXXXX";
|
||||
if (sizeof(pattern) > capacity) return 0;
|
||||
memcpy(output, pattern, sizeof(pattern));
|
||||
char temp_root[ZUPT_MAX_PATH];
|
||||
if (!realpath("/tmp", temp_root)) return 0;
|
||||
int written = snprintf(output, capacity, "%s/zupt-bench-XXXXXX",
|
||||
temp_root);
|
||||
if (written < 0 || (size_t)written >= capacity) return 0;
|
||||
if (!mkdtemp(output)) return 0;
|
||||
if (chmod(output, 0700) != 0) {
|
||||
rmdir(output);
|
||||
|
|
@ -114,7 +118,97 @@ static int zupt_create_private_temp_directory(char *output, size_t capacity) {
|
|||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
static int zupt_remove_tree_wide(const wchar_t *directory) {
|
||||
static void zupt_win_set_cleanup_errno(NTSTATUS status) {
|
||||
if (status == (NTSTATUS)0xC0000034L || /* STATUS_OBJECT_NAME_NOT_FOUND */
|
||||
status == (NTSTATUS)0xC000003AL) { /* STATUS_OBJECT_PATH_NOT_FOUND */
|
||||
errno = ENOENT;
|
||||
} else {
|
||||
errno = EACCES;
|
||||
}
|
||||
}
|
||||
|
||||
/* Open one entry relative to a pinned parent. Omitting FILE_SHARE_DELETE
|
||||
* keeps the name bound to this handle until cleanup finishes; opening the
|
||||
* reparse point itself prevents a junction or symlink from redirecting the
|
||||
* recursive walk. */
|
||||
static HANDLE zupt_win_open_cleanup_entry(HANDLE parent,
|
||||
const wchar_t *name,
|
||||
int directory_only,
|
||||
int delete_access) {
|
||||
size_t name_length = wcslen(name);
|
||||
if (name_length == 0 ||
|
||||
name_length > (size_t)USHRT_MAX / sizeof(wchar_t)) {
|
||||
errno = ENAMETOOLONG;
|
||||
return INVALID_HANDLE_VALUE;
|
||||
}
|
||||
UNICODE_STRING object_name;
|
||||
object_name.Buffer = (PWSTR)name;
|
||||
object_name.Length = (USHORT)(name_length * sizeof(wchar_t));
|
||||
object_name.MaximumLength = object_name.Length + sizeof(wchar_t);
|
||||
OBJECT_ATTRIBUTES attributes;
|
||||
InitializeObjectAttributes(&attributes, &object_name,
|
||||
OBJ_CASE_INSENSITIVE, parent, NULL);
|
||||
IO_STATUS_BLOCK status_block;
|
||||
HANDLE handle = INVALID_HANDLE_VALUE;
|
||||
ACCESS_MASK access = FILE_LIST_DIRECTORY | FILE_TRAVERSE |
|
||||
FILE_READ_ATTRIBUTES | SYNCHRONIZE;
|
||||
if (delete_access) access |= DELETE;
|
||||
ULONG share = FILE_SHARE_READ | FILE_SHARE_WRITE;
|
||||
if (delete_access) share |= FILE_SHARE_DELETE;
|
||||
ULONG options = FILE_OPEN_REPARSE_POINT | FILE_SYNCHRONOUS_IO_NONALERT;
|
||||
if (directory_only) options |= FILE_DIRECTORY_FILE;
|
||||
NTSTATUS status = NtCreateFile(
|
||||
&handle, access, &attributes, &status_block, NULL,
|
||||
FILE_ATTRIBUTE_NORMAL, share, FILE_OPEN,
|
||||
options, NULL, 0);
|
||||
if (status < 0 || handle == INVALID_HANDLE_VALUE) {
|
||||
zupt_win_set_cleanup_errno(status);
|
||||
return INVALID_HANDLE_VALUE;
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
/* Mark the exact object held by an identity-checked deletion handle. */
|
||||
static int zupt_win_delete_cleanup_handle(HANDLE handle) {
|
||||
FILE_DISPOSITION_INFO disposition;
|
||||
disposition.DeleteFile = TRUE;
|
||||
if (SetFileInformationByHandle(handle, FileDispositionInfo,
|
||||
&disposition, sizeof(disposition)))
|
||||
return 1;
|
||||
errno = EACCES;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Reopen an emptied child only after closing its no-delete-sharing traversal
|
||||
* handle. Comparing the filesystem identity before marking the new handle
|
||||
* for deletion makes a close/reopen name exchange fail safely. */
|
||||
static int zupt_win_delete_cleanup_entry(
|
||||
HANDLE parent, const wchar_t *name,
|
||||
const BY_HANDLE_FILE_INFORMATION *expected) {
|
||||
HANDLE handle = zupt_win_open_cleanup_entry(parent, name, 1, 1);
|
||||
if (handle == INVALID_HANDLE_VALUE) return 0;
|
||||
BY_HANDLE_FILE_INFORMATION current;
|
||||
int same = GetFileInformationByHandle(handle, ¤t) &&
|
||||
(current.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 &&
|
||||
(current.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0 &&
|
||||
current.dwVolumeSerialNumber == expected->dwVolumeSerialNumber &&
|
||||
current.nFileIndexHigh == expected->nFileIndexHigh &&
|
||||
current.nFileIndexLow == expected->nFileIndexLow;
|
||||
int deleted = same && zupt_win_delete_cleanup_handle(handle);
|
||||
int closed = CloseHandle(handle) != 0;
|
||||
if (!same) errno = EBUSY;
|
||||
return deleted && closed;
|
||||
}
|
||||
|
||||
static int zupt_win_plain_directory(HANDLE handle) {
|
||||
BY_HANDLE_FILE_INFORMATION info;
|
||||
return GetFileInformationByHandle(handle, &info) &&
|
||||
(info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 &&
|
||||
(info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0;
|
||||
}
|
||||
|
||||
static int zupt_remove_tree_wide(HANDLE directory_handle,
|
||||
const wchar_t *directory) {
|
||||
size_t directory_length = wcslen(directory);
|
||||
wchar_t *pattern = (wchar_t *)calloc(directory_length + 3u,
|
||||
sizeof(*pattern));
|
||||
|
|
@ -125,6 +219,8 @@ static int zupt_remove_tree_wide(const wchar_t *directory) {
|
|||
|
||||
WIN32_FIND_DATAW data;
|
||||
HANDLE search = FindFirstFileW(pattern, &data);
|
||||
DWORD search_error = search == INVALID_HANDLE_VALUE
|
||||
? GetLastError() : ERROR_SUCCESS;
|
||||
free(pattern);
|
||||
int failed = 0;
|
||||
if (search != INVALID_HANDLE_VALUE) {
|
||||
|
|
@ -143,23 +239,217 @@ static int zupt_remove_tree_wide(const wchar_t *directory) {
|
|||
child[directory_length] = L'\\';
|
||||
memcpy(child + directory_length + 1u, data.cFileName,
|
||||
(name_length + 1u) * sizeof(*child));
|
||||
if ((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
|
||||
if ((data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) {
|
||||
if (!RemoveDirectoryW(child)) failed = 1;
|
||||
} else if (zupt_remove_tree_wide(child) != 0) {
|
||||
failed = 1;
|
||||
}
|
||||
} else {
|
||||
SetFileAttributesW(child, FILE_ATTRIBUTE_NORMAL);
|
||||
if (!DeleteFileW(child)) failed = 1;
|
||||
if (DeleteFileW(child) || RemoveDirectoryW(child)) {
|
||||
free(child);
|
||||
continue;
|
||||
}
|
||||
DWORD delete_error = GetLastError();
|
||||
if (delete_error == ERROR_FILE_NOT_FOUND ||
|
||||
delete_error == ERROR_PATH_NOT_FOUND) {
|
||||
free(child);
|
||||
continue;
|
||||
}
|
||||
HANDLE child_handle = zupt_win_open_cleanup_entry(
|
||||
directory_handle, data.cFileName, 1, 0);
|
||||
if (child_handle == INVALID_HANDLE_VALUE) {
|
||||
if (errno != ENOENT) failed = 1;
|
||||
free(child);
|
||||
continue;
|
||||
}
|
||||
int child_failed = 0;
|
||||
BY_HANDLE_FILE_INFORMATION child_identity;
|
||||
if (!GetFileInformationByHandle(child_handle, &child_identity) ||
|
||||
(child_identity.dwFileAttributes &
|
||||
FILE_ATTRIBUTE_DIRECTORY) == 0 ||
|
||||
(child_identity.dwFileAttributes &
|
||||
FILE_ATTRIBUTE_REPARSE_POINT) != 0 ||
|
||||
zupt_remove_tree_wide(child_handle, child) != 0)
|
||||
child_failed = 1;
|
||||
if (!CloseHandle(child_handle)) child_failed = 1;
|
||||
if (!child_failed && !zupt_win_delete_cleanup_entry(
|
||||
directory_handle, data.cFileName, &child_identity))
|
||||
child_failed = 1;
|
||||
if (child_failed) failed = 1;
|
||||
free(child);
|
||||
} while (FindNextFileW(search, &data));
|
||||
if (GetLastError() != ERROR_NO_MORE_FILES) failed = 1;
|
||||
if (!FindClose(search)) failed = 1;
|
||||
} else if (GetLastError() != ERROR_FILE_NOT_FOUND) {
|
||||
} else if (search_error != ERROR_FILE_NOT_FOUND) {
|
||||
failed = 1;
|
||||
}
|
||||
if (!RemoveDirectoryW(directory)) failed = 1;
|
||||
return failed ? -1 : 0;
|
||||
}
|
||||
|
||||
/* Resolve the absolute temporary path one component at a time and retain
|
||||
* every ancestor handle. This makes the pathname used for enumeration
|
||||
* stable even if another process tries to exchange an ancestor directory. */
|
||||
static int zupt_win_open_cleanup_path(
|
||||
const wchar_t *directory, wchar_t full[ZUPT_MAX_PATH + 256],
|
||||
HANDLE **handles_out, size_t *handle_count_out) {
|
||||
if (!_wfullpath(full, directory, ZUPT_MAX_PATH + 256)) {
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
for (wchar_t *p = full; *p; p++) if (*p == L'/') *p = L'\\';
|
||||
if ((full[0] == L'\\' && full[1] == L'\\') ||
|
||||
!(full[0] && full[1] == L':' && full[2] == L'\\')) {
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t capacity = wcslen(full) + 1u;
|
||||
HANDLE *handles = (HANDLE *)calloc(capacity, sizeof(*handles));
|
||||
if (!handles) return 0;
|
||||
wchar_t drive_root[4] = {full[0], L':', L'\\', L'\0'};
|
||||
HANDLE current = CreateFileW(
|
||||
drive_root,
|
||||
FILE_LIST_DIRECTORY | FILE_TRAVERSE | FILE_READ_ATTRIBUTES |
|
||||
SYNCHRONIZE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
|
||||
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, NULL);
|
||||
if (current == INVALID_HANDLE_VALUE ||
|
||||
!zupt_win_plain_directory(current)) {
|
||||
DWORD open_error = current == INVALID_HANDLE_VALUE
|
||||
? GetLastError() : ERROR_ACCESS_DENIED;
|
||||
if (current != INVALID_HANDLE_VALUE) CloseHandle(current);
|
||||
free(handles);
|
||||
errno = open_error == ERROR_FILE_NOT_FOUND ||
|
||||
open_error == ERROR_PATH_NOT_FOUND
|
||||
? ENOENT : EACCES;
|
||||
return 0;
|
||||
}
|
||||
size_t count = 0;
|
||||
handles[count++] = current;
|
||||
|
||||
wchar_t *scan = full + 3;
|
||||
while (*scan) {
|
||||
wchar_t *separator = wcschr(scan, L'\\');
|
||||
if (separator) *separator = L'\0';
|
||||
HANDLE next = zupt_win_open_cleanup_entry(
|
||||
current, scan, 1, 0);
|
||||
if (separator) *separator = L'\\';
|
||||
if (next == INVALID_HANDLE_VALUE ||
|
||||
!zupt_win_plain_directory(next)) {
|
||||
if (next != INVALID_HANDLE_VALUE) CloseHandle(next);
|
||||
while (count > 0) CloseHandle(handles[--count]);
|
||||
free(handles);
|
||||
if (next != INVALID_HANDLE_VALUE) errno = EACCES;
|
||||
return 0;
|
||||
}
|
||||
handles[count++] = next;
|
||||
current = next;
|
||||
if (!separator) break;
|
||||
scan = separator + 1;
|
||||
}
|
||||
*handles_out = handles;
|
||||
*handle_count_out = count;
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
/* Resolve every component without following symlinks and return both the
|
||||
* pinned target and its pinned parent. The caller can therefore remove the
|
||||
* final directory with unlinkat() instead of resolving its pathname again. */
|
||||
static int zupt_open_temp_tree(const char *path, int *parent_out,
|
||||
int *directory_out, char *leaf,
|
||||
size_t leaf_capacity) {
|
||||
if (!path || !*path || !parent_out || !directory_out || !leaf ||
|
||||
leaf_capacity == 0) {
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
int current = open(path[0] == '/' ? "/" : ".",
|
||||
O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (current < 0) return 0;
|
||||
|
||||
const char *cursor = path;
|
||||
while (*cursor == '/') cursor++;
|
||||
while (*cursor) {
|
||||
const char *start = cursor;
|
||||
while (*cursor && *cursor != '/') cursor++;
|
||||
size_t component_length = (size_t)(cursor - start);
|
||||
while (*cursor == '/') cursor++;
|
||||
int final_component = *cursor == '\0';
|
||||
if ((component_length == 1u && start[0] == '.') ||
|
||||
component_length == 0u) {
|
||||
if (final_component) {
|
||||
close(current);
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (component_length == 2u && start[0] == '.' && start[1] == '.') {
|
||||
close(current);
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
if (component_length >= leaf_capacity) {
|
||||
close(current);
|
||||
errno = ENAMETOOLONG;
|
||||
return 0;
|
||||
}
|
||||
memcpy(leaf, start, component_length);
|
||||
leaf[component_length] = '\0';
|
||||
int next = openat(current, leaf,
|
||||
O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||
if (next < 0) {
|
||||
int saved_errno = errno;
|
||||
close(current);
|
||||
errno = saved_errno;
|
||||
return 0;
|
||||
}
|
||||
if (final_component) {
|
||||
*parent_out = current;
|
||||
*directory_out = next;
|
||||
return 1;
|
||||
}
|
||||
close(current);
|
||||
current = next;
|
||||
}
|
||||
close(current);
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Delete leaves before attempting to open them as directories. unlinkat()
|
||||
* never follows a symlink; a directory is recursively visited only through
|
||||
* an O_NOFOLLOW descriptor returned by openat(). */
|
||||
static int zupt_remove_temp_tree_fd(int directory_fd) {
|
||||
DIR *stream = fdopendir(directory_fd);
|
||||
if (!stream) {
|
||||
close(directory_fd);
|
||||
return -1;
|
||||
}
|
||||
int failed = 0;
|
||||
int parent_fd = dirfd(stream);
|
||||
for (;;) {
|
||||
errno = 0;
|
||||
struct dirent *entry = readdir(stream);
|
||||
if (!entry) {
|
||||
if (errno != 0) failed = 1;
|
||||
break;
|
||||
}
|
||||
if (strcmp(entry->d_name, ".") == 0 ||
|
||||
strcmp(entry->d_name, "..") == 0)
|
||||
continue;
|
||||
if (unlinkat(parent_fd, entry->d_name, 0) == 0 || errno == ENOENT)
|
||||
continue;
|
||||
|
||||
int child_fd = openat(parent_fd, entry->d_name,
|
||||
O_RDONLY | O_DIRECTORY | O_NOFOLLOW |
|
||||
O_CLOEXEC);
|
||||
if (child_fd < 0) {
|
||||
if (errno != ENOENT) failed = 1;
|
||||
continue;
|
||||
}
|
||||
if (zupt_remove_temp_tree_fd(child_fd) != 0) failed = 1;
|
||||
if (unlinkat(parent_fd, entry->d_name, AT_REMOVEDIR) != 0 &&
|
||||
errno != ENOENT)
|
||||
failed = 1;
|
||||
}
|
||||
if (closedir(stream) != 0) failed = 1;
|
||||
return failed ? -1 : 0;
|
||||
}
|
||||
#endif
|
||||
|
|
@ -169,41 +459,43 @@ static int zupt_remove_temp_tree(const char *directory) {
|
|||
#ifdef _WIN32
|
||||
wchar_t *wide = zupt_win_utf8_to_wide_alloc(directory);
|
||||
if (!wide) return -1;
|
||||
int result = zupt_remove_tree_wide(wide);
|
||||
wchar_t full[ZUPT_MAX_PATH + 256];
|
||||
HANDLE *handles = NULL;
|
||||
size_t handle_count = 0;
|
||||
if (!zupt_win_open_cleanup_path(wide, full, &handles, &handle_count)) {
|
||||
int result = errno == ENOENT ? 0 : -1;
|
||||
free(wide);
|
||||
return result;
|
||||
}
|
||||
HANDLE root_handle = handles[handle_count - 1u];
|
||||
int result = zupt_remove_tree_wide(root_handle, full);
|
||||
BY_HANDLE_FILE_INFORMATION root_identity;
|
||||
if (result == 0 && !GetFileInformationByHandle(root_handle,
|
||||
&root_identity))
|
||||
result = -1;
|
||||
const wchar_t *root_name = wcsrchr(full, L'\\');
|
||||
if (!root_name || root_name[1] == L'\0') result = -1;
|
||||
else root_name++;
|
||||
if (!CloseHandle(handles[--handle_count])) result = -1;
|
||||
if (result == 0 && !zupt_win_delete_cleanup_entry(
|
||||
handles[handle_count - 1u], root_name, &root_identity))
|
||||
result = -1;
|
||||
while (handle_count > 0)
|
||||
if (!CloseHandle(handles[--handle_count])) result = -1;
|
||||
free(handles);
|
||||
free(wide);
|
||||
return result;
|
||||
#else
|
||||
DIR *stream = opendir(directory);
|
||||
if (!stream) return errno == ENOENT ? 0 : -1;
|
||||
int failed = 0;
|
||||
struct dirent *entry;
|
||||
while ((entry = readdir(stream)) != NULL) {
|
||||
if (strcmp(entry->d_name, ".") == 0 ||
|
||||
strcmp(entry->d_name, "..") == 0)
|
||||
continue;
|
||||
size_t needed = strlen(directory) + strlen(entry->d_name) + 2u;
|
||||
char *child = (char *)malloc(needed);
|
||||
if (!child) {
|
||||
failed = 1;
|
||||
continue;
|
||||
}
|
||||
if (!zupt_join_temp_path(child, needed, directory, entry->d_name)) {
|
||||
free(child);
|
||||
failed = 1;
|
||||
continue;
|
||||
}
|
||||
struct stat info;
|
||||
if (lstat(child, &info) != 0) {
|
||||
failed = 1;
|
||||
} else if (S_ISDIR(info.st_mode)) {
|
||||
if (zupt_remove_temp_tree(child) != 0) failed = 1;
|
||||
} else if (unlink(child) != 0) {
|
||||
failed = 1;
|
||||
}
|
||||
free(child);
|
||||
}
|
||||
if (closedir(stream) != 0) failed = 1;
|
||||
if (rmdir(directory) != 0) failed = 1;
|
||||
int parent_fd = -1;
|
||||
int directory_fd = -1;
|
||||
char leaf[ZUPT_MAX_PATH];
|
||||
if (!zupt_open_temp_tree(directory, &parent_fd, &directory_fd,
|
||||
leaf, sizeof(leaf)))
|
||||
return errno == ENOENT ? 0 : -1;
|
||||
int failed = zupt_remove_temp_tree_fd(directory_fd) != 0;
|
||||
if (unlinkat(parent_fd, leaf, AT_REMOVEDIR) != 0 && errno != ENOENT)
|
||||
failed = 1;
|
||||
if (close(parent_fd) != 0) failed = 1;
|
||||
return failed ? -1 : 0;
|
||||
#endif
|
||||
}
|
||||
|
|
@ -429,7 +721,11 @@ static int prompt_password(const char *prompt, char *buf, size_t cap) {
|
|||
if (!buf || cap < 2) return 0;
|
||||
buf[0] = '\0';
|
||||
#ifdef _WIN32
|
||||
if (!_isatty(_fileno(stdin))) {
|
||||
HANDLE input_handle = GetStdHandle(STD_INPUT_HANDLE);
|
||||
DWORD input_mode = 0;
|
||||
if (input_handle == NULL || input_handle == INVALID_HANDLE_VALUE ||
|
||||
GetFileType(input_handle) != FILE_TYPE_CHAR ||
|
||||
!GetConsoleMode(input_handle, &input_mode)) {
|
||||
fprintf(stderr, "Error: password prompt requires a terminal.\n");
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -445,6 +741,11 @@ static int prompt_password(const char *prompt, char *buf, size_t cap) {
|
|||
int too_long = 0;
|
||||
for (;;) {
|
||||
int c = _getch();
|
||||
if (c == EOF) {
|
||||
zupt_secure_wipe(buf, cap);
|
||||
fprintf(stderr, "\nError: cannot read password prompt.\n");
|
||||
return 0;
|
||||
}
|
||||
if (c == '\r' || c == '\n') break;
|
||||
if (c == 0 || c == 0xe0) {
|
||||
(void)_getch();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@
|
|||
set -Eeuo pipefail
|
||||
|
||||
bin=${1:-./zupt}
|
||||
repo_root=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd -P)
|
||||
case $bin in
|
||||
/*) ;;
|
||||
*) bin="$(pwd -P)/${bin#./}" ;;
|
||||
esac
|
||||
tmp=$(mktemp -d "${TMPDIR:-/tmp}/zupt-bench-safety.XXXXXXXX")
|
||||
trap 'rm -rf -- "$tmp"' EXIT HUP INT TERM
|
||||
|
||||
|
|
@ -11,9 +16,36 @@ fail() {
|
|||
exit 1
|
||||
}
|
||||
|
||||
# CodeQL #7 reported the old lstat(child) -> recursive pathname operation as
|
||||
# cpp/toctou-race-condition. Keep the platform-specific cleanup primitives in
|
||||
# the source gate as well as exercising the runtime symlink boundary below.
|
||||
cleanup_source=$repo_root/src/zupt_main.c
|
||||
grep -Fq 'static int zupt_remove_temp_tree_fd(int directory_fd)' \
|
||||
"$cleanup_source" || fail 'POSIX descriptor-relative cleanup is missing'
|
||||
grep -Fq 'unlinkat(parent_fd, entry->d_name, 0)' "$cleanup_source" ||
|
||||
fail 'POSIX leaf cleanup is not unlinkat-relative'
|
||||
grep -Fq 'directory_handle, data.cFileName, 1, 0)' "$cleanup_source" ||
|
||||
fail 'Windows recursive cleanup is not handle-relative'
|
||||
grep -Fq 'FILE_OPEN_REPARSE_POINT' "$cleanup_source" ||
|
||||
fail 'Windows cleanup no longer opens reparse points without following'
|
||||
grep -Fq 'zupt_win_delete_cleanup_entry(' "$cleanup_source" ||
|
||||
fail 'Windows cleanup lacks identity-checked handle deletion'
|
||||
grep -Fq 'current.nFileIndexLow == expected->nFileIndexLow' "$cleanup_source" ||
|
||||
fail 'Windows cleanup no longer rejects a close/reopen name exchange'
|
||||
if grep -Fq 'RemoveDirectoryW(full)' "$cleanup_source"; then
|
||||
fail 'Windows root cleanup restored post-handle pathname deletion'
|
||||
fi
|
||||
if grep -Fq 'lstat(child' "$cleanup_source" ||
|
||||
grep -Fq 'zupt_remove_temp_tree(child' "$cleanup_source"; then
|
||||
fail 'temporary cleanup restored a check-then-use pathname traversal'
|
||||
fi
|
||||
|
||||
case $(uname -s 2>/dev/null || printf unknown) in
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
printf 'SKIP: historical POSIX /tmp symlink benchmark test is not native on Windows\n'
|
||||
"$bin" bench --compare >/dev/null 2>&1 ||
|
||||
fail 'native Windows handle-relative benchmark cleanup failed'
|
||||
printf 'SKIP: adversarial POSIX symlink injection is not native on Windows\n'
|
||||
printf 'private Windows handle-relative benchmark workspace: PASS\n'
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
|
@ -46,4 +78,50 @@ if [[ -d $old_directory ]]; then
|
|||
mv "$old_directory" "$tmp/historical-remnant"
|
||||
fi
|
||||
|
||||
printf 'private benchmark workspace: PASS\n'
|
||||
# Inject a directory symlink into the private workspace while a real benchmark
|
||||
# is active. Cleanup must remove the link itself and never visit its target.
|
||||
mkdir "$tmp/symlink-target"
|
||||
printf 'cleanup sentinel must survive\n' > "$tmp/symlink-target/sentinel"
|
||||
cp "$tmp/symlink-target/sentinel" "$tmp/symlink-target.expected"
|
||||
dd if=/dev/urandom of="$tmp/injection-input" bs=65536 count=128 2>/dev/null
|
||||
|
||||
physical_tmp=$(CDPATH='' cd -P -- /tmp && pwd -P)
|
||||
: > "$tmp/preexisting-workspaces"
|
||||
for candidate in "$physical_tmp"/zupt-bench-*; do
|
||||
if [[ -d $candidate && ! -L $candidate ]]; then
|
||||
printf '%s\n' "$candidate" >> "$tmp/preexisting-workspaces"
|
||||
fi
|
||||
done
|
||||
|
||||
(cd "$tmp" && "$bin" bench injection-input >/dev/null 2>&1) &
|
||||
bench_pid=$!
|
||||
injected=0
|
||||
injected_workspace=
|
||||
attempt=0
|
||||
while (( attempt < 1000 )); do
|
||||
for candidate in "$physical_tmp"/zupt-bench-*; do
|
||||
[[ -d $candidate && ! -L $candidate ]] || continue
|
||||
if grep -Fqx -- "$candidate" "$tmp/preexisting-workspaces"; then
|
||||
continue
|
||||
fi
|
||||
if ln -s "$tmp/symlink-target" "$candidate/attacker-link" \
|
||||
2>/dev/null; then
|
||||
injected=1
|
||||
injected_workspace=$candidate
|
||||
break
|
||||
fi
|
||||
done
|
||||
(( injected == 1 )) && break
|
||||
kill -0 "$bench_pid" 2>/dev/null || break
|
||||
sleep 0.01
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
wait "$bench_pid" || fail 'benchmark with injected symlink failed'
|
||||
(( injected == 1 )) || fail 'could not observe the private benchmark workspace'
|
||||
if [[ -e $injected_workspace || -L $injected_workspace ]]; then
|
||||
fail 'injected workspace was not the benchmark tree that was removed'
|
||||
fi
|
||||
cmp "$tmp/symlink-target.expected" "$tmp/symlink-target/sentinel" ||
|
||||
fail 'temporary cleanup followed an injected directory symlink'
|
||||
|
||||
printf 'private descriptor/handle-relative benchmark workspace: PASS\n'
|
||||
|
|
|
|||
|
|
@ -56,9 +56,30 @@ if "$binary" test --pass-fd not-a-number archive.zupt >/dev/null 2>&1; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
if "$binary" test --password-prompt archive.zupt </dev/null >/dev/null 2>&1; then
|
||||
prompt_log=$test_root/non-interactive-prompt.log
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
set +e
|
||||
timeout 10 "$binary" test --password-prompt archive.zupt \
|
||||
</dev/null >"$prompt_log" 2>&1
|
||||
prompt_status=$?
|
||||
set -e
|
||||
else
|
||||
set +e
|
||||
"$binary" test --password-prompt archive.zupt \
|
||||
</dev/null >"$prompt_log" 2>&1
|
||||
prompt_status=$?
|
||||
set -e
|
||||
fi
|
||||
if ((prompt_status == 124)); then
|
||||
printf '%s\n' 'FAIL: non-interactive password prompt timed out' >&2
|
||||
exit 1
|
||||
elif ((prompt_status == 0)); then
|
||||
printf '%s\n' 'FAIL: non-interactive password prompt unexpectedly succeeded' >&2
|
||||
exit 1
|
||||
elif ! grep -Fq 'password prompt requires a terminal.' "$prompt_log"; then
|
||||
printf 'FAIL: non-interactive password prompt returned status %d without a terminal rejection\n' \
|
||||
"$prompt_status" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case $(uname -s) in
|
||||
|
|
|
|||
|
|
@ -154,16 +154,19 @@ case "$(uname -s)" in
|
|||
|
||||
tree=$(fresh_tree raw-c1-path)
|
||||
control_name=$'raw-\200.txt'
|
||||
printf '\177ELF\002\001\001\000compiled' >"$tree/$control_name"
|
||||
if "$SCANNER" --tree "$tree" >"$TEST_TMP/output" 2>&1; then
|
||||
printf 'not ok - raw C1 path was not rejected\n'
|
||||
exit 1
|
||||
elif ! grep -Fq 'raw-\x80.txt' "$TEST_TMP/output" ||
|
||||
LC_ALL=C grep -q $'\200' "$TEST_TMP/output"; then
|
||||
printf 'not ok - raw C1 path was not rendered safely\n'
|
||||
exit 1
|
||||
if { printf '\177ELF\002\001\001\000compiled' >"$tree/$control_name"; } 2>/dev/null; then
|
||||
if "$SCANNER" --tree "$tree" >"$TEST_TMP/output" 2>&1; then
|
||||
printf 'not ok - raw C1 path was not rejected\n'
|
||||
exit 1
|
||||
elif ! grep -Fq 'raw-\x80.txt' "$TEST_TMP/output" ||
|
||||
LC_ALL=C grep -q $'\200' "$TEST_TMP/output"; then
|
||||
printf 'not ok - raw C1 path was not rendered safely\n'
|
||||
exit 1
|
||||
else
|
||||
pass 'scanner escapes invalid raw C1 bytes in reported paths'
|
||||
fi
|
||||
else
|
||||
pass 'scanner escapes invalid raw C1 bytes in reported paths'
|
||||
skip 'raw C1 filenames are forbidden by this filesystem'
|
||||
fi
|
||||
|
||||
tree=$(fresh_tree utf8-c1-path)
|
||||
|
|
|
|||
|
|
@ -187,6 +187,31 @@ else
|
|||
F "ECHO bit-clear missing the explicit (tcflag_t) cast"
|
||||
fi
|
||||
|
||||
# A restore to a device is irreversible. Classify the already-open descriptor
|
||||
# rather than checking target_path and resolving that mutable name again.
|
||||
if grep -Fq 'lstat(target_path' src/zupt_disk.c; then
|
||||
F "disk restore has a path-check/open TOCTOU pattern"
|
||||
elif grep -Fq 'tgt_fd = open(target_path' src/zupt_disk.c &&
|
||||
grep -Fq 'fstat(tgt_fd, &opened_st)' src/zupt_disk.c; then
|
||||
P "disk restore classifies the opened target descriptor"
|
||||
else
|
||||
F "disk restore descriptor-first target guard is missing"
|
||||
fi
|
||||
|
||||
# CodeQL #5 reported chmod(dst, mode) after reopening/resolving the SDK save
|
||||
# path. Key copies must use the core's handle/descriptor-relative atomic
|
||||
# publisher and apply POSIX permissions to its already-open temporary stream.
|
||||
if grep -Fq 'chmod(dst, mode)' sdk/src/zuptsdk.c; then
|
||||
F "SDK key save has a path-based chmod TOCTOU pattern"
|
||||
elif grep -Fq 'zupt_atomic_output_open(dst, &fo)' sdk/src/zuptsdk.c &&
|
||||
grep -Fq 'fchmod(fileno(fo), mode)' sdk/src/zuptsdk.c &&
|
||||
grep -Fq 'zupt_atomic_output_finish(output, rc == ZUPTSDK_OK)' \
|
||||
sdk/src/zuptsdk.c; then
|
||||
P "SDK key save uses descriptor-relative atomic publication"
|
||||
else
|
||||
F "SDK key save atomic publication guard is missing"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " ───────────────────────────────────────"
|
||||
echo " Static analysis: $PASS passed, $FAIL failed"
|
||||
|
|
|
|||
Loading…
Reference in a new issue