security: close filesystem race findings

This commit is contained in:
Cristian Cezar Moisés 2026-08-31 20:27:14 -03:00
commit d8668e6d64
10 changed files with 698 additions and 133 deletions

View file

@ -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;
}

View file

@ -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);
}