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
|
run: make CC=${{ matrix.cc }} V=1 WITH_SDK=0 WITH_PQBOX=0 check
|
||||||
- name: Extended upstream tests
|
- name: Extended upstream tests
|
||||||
run: make CC=${{ matrix.cc }} V=1 WITH_SDK=0 WITH_PQBOX=0 test-all
|
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
|
- name: Functional test of the built CLI
|
||||||
run: bash scripts/test-installed-zupt.sh "$PWD/zupt"
|
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.
|
# Release-only gates need a committed Git checkout and packaging metadata.
|
||||||
# Keep them out of downstream %check, which intentionally has no dist rebuild.
|
# Keep them out of downstream %check, which intentionally has no dist rebuild.
|
||||||
release-check: test-all audit-licenses
|
release-check: test-all audit-licenses
|
||||||
|
$(Q)$(MAKE) sdk-test
|
||||||
$(Q)bash tests/test_static_analysis.sh
|
$(Q)bash tests/test_static_analysis.sh
|
||||||
$(Q)bash tests/test_packaging_syntax.sh
|
$(Q)bash tests/test_packaging_syntax.sh
|
||||||
$(Q)bash scripts/test-installed-zupt.sh ./$(TARGET)
|
$(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) {
|
static int zsdk_copy_file(const char *src, const char *dst, mode_t mode) {
|
||||||
FILE *fi = fopen(src, "rb");
|
FILE *fi = fopen(src, "rb");
|
||||||
if (!fi) return ZSDK_FAIL(ZUPTSDK_ERR_IO, "open %s", src);
|
if (!fi) return ZSDK_FAIL(ZUPTSDK_ERR_IO, "open %s", src);
|
||||||
FILE *fo = fopen(dst, "wb");
|
FILE *fo = NULL;
|
||||||
if (!fo) { fclose(fi); return ZSDK_FAIL(ZUPTSDK_ERR_IO, "create %s", dst); }
|
zupt_atomic_output_t *output = zupt_atomic_output_open(dst, &fo);
|
||||||
uint8_t buf[4096];
|
if (!output) {
|
||||||
size_t n;
|
int saved_errno = errno;
|
||||||
int rc = ZUPTSDK_OK;
|
fclose(fi);
|
||||||
while ((n = fread(buf, 1, sizeof(buf), fi)) > 0)
|
errno = saved_errno;
|
||||||
if (fwrite(buf, 1, n, fo) != n) { rc = ZSDK_FAIL(ZUPTSDK_ERR_IO, "write %s", dst); break; }
|
return ZSDK_FAIL(ZUPTSDK_ERR_IO, "create %s", dst);
|
||||||
zuptsdk_secure_zero(buf, sizeof(buf));
|
}
|
||||||
fclose(fi); fclose(fo);
|
|
||||||
#ifndef _WIN32
|
#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
|
#else
|
||||||
(void)mode;
|
(void)mode;
|
||||||
#endif
|
#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;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#ifndef _WIN32
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#endif
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
static int g_pass = 0, g_fail = 0;
|
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. "
|
"Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod. "
|
||||||
"End of test data.\n";
|
"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) {
|
static void test_version(void) {
|
||||||
TEST("version_string returns non-NULL");
|
TEST("version_string returns non-NULL");
|
||||||
const char *v = zuptsdk_version_string();
|
const char *v = zuptsdk_version_string();
|
||||||
|
|
@ -250,6 +326,15 @@ cleanup:
|
||||||
|
|
||||||
static void test_keypair_pq(void) {
|
static void test_keypair_pq(void) {
|
||||||
TEST("keypair_generate + compress_pq + extract_pq");
|
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;
|
zuptsdk_ctx_t *ctx = NULL;
|
||||||
CHECK(zuptsdk_ctx_create(&ctx), "ctx");
|
CHECK(zuptsdk_ctx_create(&ctx), "ctx");
|
||||||
|
|
||||||
|
|
@ -257,17 +342,35 @@ static void test_keypair_pq(void) {
|
||||||
int rc = zuptsdk_keypair_generate(ctx, &kp);
|
int rc = zuptsdk_keypair_generate(ctx, &kp);
|
||||||
if (rc != ZUPTSDK_OK) { FAIL("keygen"); zuptsdk_ctx_destroy(ctx); return; }
|
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 */
|
/* 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; }
|
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; }
|
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_pubkey_t *pub = NULL;
|
||||||
zuptsdk_privkey_t *priv = 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; }
|
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; }
|
if (rc != ZUPTSDK_OK) { FAIL("load priv"); zuptsdk_pubkey_destroy(pub); goto err; }
|
||||||
|
|
||||||
zuptsdk_options_t *opts = NULL;
|
zuptsdk_options_t *opts = NULL;
|
||||||
|
|
@ -295,8 +398,8 @@ static void test_keypair_pq(void) {
|
||||||
zuptsdk_privkey_destroy(priv);
|
zuptsdk_privkey_destroy(priv);
|
||||||
zuptsdk_options_destroy(opts);
|
zuptsdk_options_destroy(opts);
|
||||||
|
|
||||||
unlink("/tmp/_zsdk_priv.key");
|
unlink(saved_priv);
|
||||||
unlink("/tmp/_zsdk_pub.key");
|
unlink(saved_pub);
|
||||||
|
|
||||||
if (!ok) { FAIL("byte mismatch or rc != OK"); zuptsdk_keypair_destroy(kp); zuptsdk_ctx_destroy(ctx); return; }
|
if (!ok) { FAIL("byte mismatch or rc != OK"); zuptsdk_keypair_destroy(kp); zuptsdk_ctx_destroy(ctx); return; }
|
||||||
zuptsdk_keypair_destroy(kp);
|
zuptsdk_keypair_destroy(kp);
|
||||||
|
|
@ -305,6 +408,8 @@ static void test_keypair_pq(void) {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
err:
|
err:
|
||||||
|
unlink(saved_priv);
|
||||||
|
unlink(saved_pub);
|
||||||
zuptsdk_keypair_destroy(kp);
|
zuptsdk_keypair_destroy(kp);
|
||||||
zuptsdk_ctx_destroy(ctx);
|
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
|
#else
|
||||||
int tgt_fd = -1;
|
int tgt_fd = -1;
|
||||||
int is_block_dev = 0;
|
int is_block_dev = 0;
|
||||||
struct stat target_st;
|
/* Resolve the target exactly once before making any type or identity
|
||||||
|
* decision. The open is non-truncating, O_NOFOLLOW rejects a final
|
||||||
if (lstat(target_path, &target_st) == 0) {
|
* symlink, and fstat classifies the kernel object that was actually
|
||||||
if (S_ISLNK(target_st.st_mode)) {
|
* opened. Device restores retain this same descriptor through the final
|
||||||
fprintf(stderr, "Error: refusing a symbolic-link restore target.\n");
|
* write, so a concurrent pathname exchange cannot redirect the restore. */
|
||||||
fclose(f);
|
tgt_fd = open(target_path, O_WRONLY | O_NOFOLLOW | O_CLOEXEC |
|
||||||
return ZUPT_ERR_INVALID;
|
O_NONBLOCK | O_SYNC);
|
||||||
}
|
if (tgt_fd >= 0) {
|
||||||
if (S_ISREG(target_st.st_mode)) {
|
struct stat opened_st;
|
||||||
if (target_st.st_dev == archive_identity.device &&
|
if (fstat(tgt_fd, &opened_st) != 0) {
|
||||||
target_st.st_ino == archive_identity.inode) {
|
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,
|
fprintf(stderr,
|
||||||
"Error: archive and restore target are the same file.\n");
|
"Error: archive and restore target are the same file.\n");
|
||||||
fclose(f);
|
fclose(f);
|
||||||
return ZUPT_ERR_INVALID;
|
return ZUPT_ERR_INVALID;
|
||||||
}
|
}
|
||||||
if (target_st.st_nlink != 1) {
|
if (opened_st.st_nlink != 1) {
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"Error: refusing a multiply-linked restore target.\n");
|
"Error: refusing a multiply-linked restore target.\n");
|
||||||
fclose(f);
|
fclose(f);
|
||||||
|
|
@ -1244,59 +1256,46 @@ zupt_error_t zupt_disk_restore(const char *archive_path, const char *target_path
|
||||||
}
|
}
|
||||||
target_atomic =
|
target_atomic =
|
||||||
zupt_atomic_output_open(target_path, &target_stream);
|
zupt_atomic_output_open(target_path, &target_stream);
|
||||||
} else if (S_ISBLK(target_st.st_mode) ||
|
} else if (S_ISBLK(opened_st.st_mode) ||
|
||||||
S_ISCHR(target_st.st_mode)) {
|
S_ISCHR(opened_st.st_mode)) {
|
||||||
tgt_fd = open(target_path, O_WRONLY | O_NOFOLLOW | O_CLOEXEC |
|
int flags = fcntl(tgt_fd, F_GETFL);
|
||||||
O_NONBLOCK | O_SYNC);
|
if (flags < 0 ||
|
||||||
if (tgt_fd >= 0) {
|
fcntl(tgt_fd, F_SETFL, flags & ~O_NONBLOCK) != 0) {
|
||||||
struct stat opened_st;
|
close(tgt_fd);
|
||||||
if (fstat(tgt_fd, &opened_st) != 0 ||
|
tgt_fd = -1;
|
||||||
opened_st.st_dev != target_st.st_dev ||
|
} else {
|
||||||
opened_st.st_ino != target_st.st_ino ||
|
#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
|
||||||
!(S_ISBLK(opened_st.st_mode) ||
|
uint64_t target_capacity = 0;
|
||||||
S_ISCHR(opened_st.st_mode))) {
|
if (!disk_restore_target_capacity(
|
||||||
|
tgt_fd, &opened_st, &target_capacity)) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"Error: cannot determine restore device "
|
||||||
|
"capacity safely.\n");
|
||||||
close(tgt_fd);
|
close(tgt_fd);
|
||||||
tgt_fd = -1;
|
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 {
|
} else {
|
||||||
int flags = fcntl(tgt_fd, F_GETFL);
|
is_block_dev = 1;
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
fprintf(stderr,
|
||||||
|
"Error: restore-device capacity queries are "
|
||||||
|
"not supported on this platform.\n");
|
||||||
|
close(tgt_fd);
|
||||||
|
tgt_fd = -1;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
close(tgt_fd);
|
||||||
|
tgt_fd = -1;
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"Error: restore target is not a regular file or device.\n");
|
"Error: restore target is not a regular file or device.\n");
|
||||||
fclose(f);
|
fclose(f);
|
||||||
|
|
@ -1304,8 +1303,12 @@ zupt_error_t zupt_disk_restore(const char *archive_path, const char *target_path
|
||||||
}
|
}
|
||||||
} else if (errno == ENOENT) {
|
} else if (errno == ENOENT) {
|
||||||
target_atomic = zupt_atomic_output_open(target_path, &target_stream);
|
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 {
|
} else {
|
||||||
fprintf(stderr, "Error: Cannot inspect target '%s': %s\n",
|
fprintf(stderr, "Error: Cannot open target '%s': %s\n",
|
||||||
target_path, strerror(errno));
|
target_path, strerror(errno));
|
||||||
fclose(f);
|
fclose(f);
|
||||||
return ZUPT_ERR_IO;
|
return ZUPT_ERR_IO;
|
||||||
|
|
|
||||||
397
src/zupt_main.c
397
src/zupt_main.c
|
|
@ -25,7 +25,9 @@
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <conio.h>
|
#include <conio.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
#include <winternl.h>
|
||||||
#else
|
#else
|
||||||
|
#include <fcntl.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <termios.h>
|
#include <termios.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -100,9 +102,11 @@ static int zupt_create_private_temp_directory(char *output, size_t capacity) {
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
#else
|
#else
|
||||||
static const char pattern[] = "/tmp/zupt-bench-XXXXXX";
|
char temp_root[ZUPT_MAX_PATH];
|
||||||
if (sizeof(pattern) > capacity) return 0;
|
if (!realpath("/tmp", temp_root)) return 0;
|
||||||
memcpy(output, pattern, sizeof(pattern));
|
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 (!mkdtemp(output)) return 0;
|
||||||
if (chmod(output, 0700) != 0) {
|
if (chmod(output, 0700) != 0) {
|
||||||
rmdir(output);
|
rmdir(output);
|
||||||
|
|
@ -114,7 +118,97 @@ static int zupt_create_private_temp_directory(char *output, size_t capacity) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef _WIN32
|
#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);
|
size_t directory_length = wcslen(directory);
|
||||||
wchar_t *pattern = (wchar_t *)calloc(directory_length + 3u,
|
wchar_t *pattern = (wchar_t *)calloc(directory_length + 3u,
|
||||||
sizeof(*pattern));
|
sizeof(*pattern));
|
||||||
|
|
@ -125,6 +219,8 @@ static int zupt_remove_tree_wide(const wchar_t *directory) {
|
||||||
|
|
||||||
WIN32_FIND_DATAW data;
|
WIN32_FIND_DATAW data;
|
||||||
HANDLE search = FindFirstFileW(pattern, &data);
|
HANDLE search = FindFirstFileW(pattern, &data);
|
||||||
|
DWORD search_error = search == INVALID_HANDLE_VALUE
|
||||||
|
? GetLastError() : ERROR_SUCCESS;
|
||||||
free(pattern);
|
free(pattern);
|
||||||
int failed = 0;
|
int failed = 0;
|
||||||
if (search != INVALID_HANDLE_VALUE) {
|
if (search != INVALID_HANDLE_VALUE) {
|
||||||
|
|
@ -143,23 +239,217 @@ static int zupt_remove_tree_wide(const wchar_t *directory) {
|
||||||
child[directory_length] = L'\\';
|
child[directory_length] = L'\\';
|
||||||
memcpy(child + directory_length + 1u, data.cFileName,
|
memcpy(child + directory_length + 1u, data.cFileName,
|
||||||
(name_length + 1u) * sizeof(*child));
|
(name_length + 1u) * sizeof(*child));
|
||||||
if ((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {
|
if (DeleteFileW(child) || RemoveDirectoryW(child)) {
|
||||||
if ((data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) {
|
free(child);
|
||||||
if (!RemoveDirectoryW(child)) failed = 1;
|
continue;
|
||||||
} else if (zupt_remove_tree_wide(child) != 0) {
|
|
||||||
failed = 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SetFileAttributesW(child, FILE_ATTRIBUTE_NORMAL);
|
|
||||||
if (!DeleteFileW(child)) failed = 1;
|
|
||||||
}
|
}
|
||||||
|
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);
|
free(child);
|
||||||
} while (FindNextFileW(search, &data));
|
} while (FindNextFileW(search, &data));
|
||||||
|
if (GetLastError() != ERROR_NO_MORE_FILES) failed = 1;
|
||||||
if (!FindClose(search)) failed = 1;
|
if (!FindClose(search)) failed = 1;
|
||||||
} else if (GetLastError() != ERROR_FILE_NOT_FOUND) {
|
} else if (search_error != ERROR_FILE_NOT_FOUND) {
|
||||||
failed = 1;
|
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;
|
return failed ? -1 : 0;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -169,41 +459,43 @@ static int zupt_remove_temp_tree(const char *directory) {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
wchar_t *wide = zupt_win_utf8_to_wide_alloc(directory);
|
wchar_t *wide = zupt_win_utf8_to_wide_alloc(directory);
|
||||||
if (!wide) return -1;
|
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);
|
free(wide);
|
||||||
return result;
|
return result;
|
||||||
#else
|
#else
|
||||||
DIR *stream = opendir(directory);
|
int parent_fd = -1;
|
||||||
if (!stream) return errno == ENOENT ? 0 : -1;
|
int directory_fd = -1;
|
||||||
int failed = 0;
|
char leaf[ZUPT_MAX_PATH];
|
||||||
struct dirent *entry;
|
if (!zupt_open_temp_tree(directory, &parent_fd, &directory_fd,
|
||||||
while ((entry = readdir(stream)) != NULL) {
|
leaf, sizeof(leaf)))
|
||||||
if (strcmp(entry->d_name, ".") == 0 ||
|
return errno == ENOENT ? 0 : -1;
|
||||||
strcmp(entry->d_name, "..") == 0)
|
int failed = zupt_remove_temp_tree_fd(directory_fd) != 0;
|
||||||
continue;
|
if (unlinkat(parent_fd, leaf, AT_REMOVEDIR) != 0 && errno != ENOENT)
|
||||||
size_t needed = strlen(directory) + strlen(entry->d_name) + 2u;
|
failed = 1;
|
||||||
char *child = (char *)malloc(needed);
|
if (close(parent_fd) != 0) failed = 1;
|
||||||
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;
|
|
||||||
return failed ? -1 : 0;
|
return failed ? -1 : 0;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
@ -429,7 +721,11 @@ static int prompt_password(const char *prompt, char *buf, size_t cap) {
|
||||||
if (!buf || cap < 2) return 0;
|
if (!buf || cap < 2) return 0;
|
||||||
buf[0] = '\0';
|
buf[0] = '\0';
|
||||||
#ifdef _WIN32
|
#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");
|
fprintf(stderr, "Error: password prompt requires a terminal.\n");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -445,6 +741,11 @@ static int prompt_password(const char *prompt, char *buf, size_t cap) {
|
||||||
int too_long = 0;
|
int too_long = 0;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
int c = _getch();
|
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 == '\r' || c == '\n') break;
|
||||||
if (c == 0 || c == 0xe0) {
|
if (c == 0 || c == 0xe0) {
|
||||||
(void)_getch();
|
(void)_getch();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,11 @@
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
bin=${1:-./zupt}
|
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")
|
tmp=$(mktemp -d "${TMPDIR:-/tmp}/zupt-bench-safety.XXXXXXXX")
|
||||||
trap 'rm -rf -- "$tmp"' EXIT HUP INT TERM
|
trap 'rm -rf -- "$tmp"' EXIT HUP INT TERM
|
||||||
|
|
||||||
|
|
@ -11,9 +16,36 @@ fail() {
|
||||||
exit 1
|
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
|
case $(uname -s 2>/dev/null || printf unknown) in
|
||||||
MINGW*|MSYS*|CYGWIN*)
|
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
|
exit 0
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
@ -46,4 +78,50 @@ if [[ -d $old_directory ]]; then
|
||||||
mv "$old_directory" "$tmp/historical-remnant"
|
mv "$old_directory" "$tmp/historical-remnant"
|
||||||
fi
|
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
|
exit 1
|
||||||
fi
|
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
|
printf '%s\n' 'FAIL: non-interactive password prompt unexpectedly succeeded' >&2
|
||||||
exit 1
|
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
|
fi
|
||||||
|
|
||||||
case $(uname -s) in
|
case $(uname -s) in
|
||||||
|
|
|
||||||
|
|
@ -154,16 +154,19 @@ case "$(uname -s)" in
|
||||||
|
|
||||||
tree=$(fresh_tree raw-c1-path)
|
tree=$(fresh_tree raw-c1-path)
|
||||||
control_name=$'raw-\200.txt'
|
control_name=$'raw-\200.txt'
|
||||||
printf '\177ELF\002\001\001\000compiled' >"$tree/$control_name"
|
if { printf '\177ELF\002\001\001\000compiled' >"$tree/$control_name"; } 2>/dev/null; then
|
||||||
if "$SCANNER" --tree "$tree" >"$TEST_TMP/output" 2>&1; then
|
if "$SCANNER" --tree "$tree" >"$TEST_TMP/output" 2>&1; then
|
||||||
printf 'not ok - raw C1 path was not rejected\n'
|
printf 'not ok - raw C1 path was not rejected\n'
|
||||||
exit 1
|
exit 1
|
||||||
elif ! grep -Fq 'raw-\x80.txt' "$TEST_TMP/output" ||
|
elif ! grep -Fq 'raw-\x80.txt' "$TEST_TMP/output" ||
|
||||||
LC_ALL=C grep -q $'\200' "$TEST_TMP/output"; then
|
LC_ALL=C grep -q $'\200' "$TEST_TMP/output"; then
|
||||||
printf 'not ok - raw C1 path was not rendered safely\n'
|
printf 'not ok - raw C1 path was not rendered safely\n'
|
||||||
exit 1
|
exit 1
|
||||||
|
else
|
||||||
|
pass 'scanner escapes invalid raw C1 bytes in reported paths'
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
pass 'scanner escapes invalid raw C1 bytes in reported paths'
|
skip 'raw C1 filenames are forbidden by this filesystem'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
tree=$(fresh_tree utf8-c1-path)
|
tree=$(fresh_tree utf8-c1-path)
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,31 @@ else
|
||||||
F "ECHO bit-clear missing the explicit (tcflag_t) cast"
|
F "ECHO bit-clear missing the explicit (tcflag_t) cast"
|
||||||
fi
|
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 " ───────────────────────────────────────"
|
echo " ───────────────────────────────────────"
|
||||||
echo " Static analysis: $PASS passed, $FAIL failed"
|
echo " Static analysis: $PASS passed, $FAIL failed"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue