fix(sync): reject truncated native artifacts

This commit is contained in:
fy59 2026-09-17 15:19:59 +02:00
parent d29532a735
commit 2c5a8be282
9 changed files with 854 additions and 446 deletions

View file

@ -5,6 +5,7 @@ import json, os, signal, socket, sqlite3, subprocess, tempfile, time, unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BUILD = ROOT / os.environ.get("TRAINLOG_WEB_E2E_BUILD", "build") / "tui"
GECKO = Path(
os.environ.get("TRAINLOG_GECKODRIVER", "/home/fy59/.cache/trainlog/web-e2e-tools/geckodriver")
)
@ -114,13 +115,13 @@ class BrowserSyncTest(unittest.TestCase):
env.update(
{
"TRAINLOG_SYNC_MTP_ADAPTER": str(
ROOT / "build/tui/generation-mtp-adapter-double"
BUILD / "generation-mtp-adapter-double"
),
"TRAINLOG_MTP_DOUBLE_ROOT": str(root / "device"),
}
)
server = subprocess.Popen(
[str(ROOT / "build/tui/trainlog"), "--web", "--port", str(port)],
[str(BUILD / "trainlog"), "--web", "--port", str(port)],
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,

View file

@ -72,7 +72,11 @@ def run_adapter(
diagnostic = (
result.stderr.decode("utf-8", "replace")[:1024] or "MTP adapter failed"
).strip()
if allow_missing_peer and "expected peer was not found" in diagnostic:
missing_peer = diagnostic in {
"double failed 6 expected Android peer not found",
"MTP adapter failed status=6 diagnostic=expected Android peer not found",
}
if allow_missing_peer and missing_peer:
return False
raise RuntimeError(diagnostic)
return True
@ -276,4 +280,8 @@ def main() -> int:
if __name__ == "__main__":
raise SystemExit(main())
try:
raise SystemExit(main())
except RuntimeError as error:
print(f"sync peer failed: {error}", file=sys.stderr)
raise SystemExit(1) from None

View file

@ -2,17 +2,25 @@
#define TRAINLOG_WEB_SERVER_H
#include <signal.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "trainlog/database.h"
/* Serialize the exact successful sync-admission document. Both identities are
* borrowed, validated sy_<uuid-v4> values. On failure output is empty. */
bool trainlog_web_sync_accepted_serialize(
const char *request_id, const char *run_id, char *output, size_t capacity, size_t *output_size);
/* Blocking, single-threaded local HTTP adapter. Port zero is reserved for
* controlled test harnesses and is rejected by the CLI. The database remains
* borrowed and process-owned; this infrastructure route never reads it. */
int trainlog_web_server_run(TrainlogDatabase *database, const char *database_path,
uint16_t port,
const volatile sig_atomic_t *stop_requested,
char *diagnostic, size_t diagnostic_capacity);
int trainlog_web_server_run(TrainlogDatabase *database,
const char *database_path,
uint16_t port,
const volatile sig_atomic_t *stop_requested,
char *diagnostic,
size_t diagnostic_capacity);
#endif

File diff suppressed because it is too large Load diff

33
tui/tests/bounded_path.h Normal file
View file

@ -0,0 +1,33 @@
#ifndef TRAINLOG_TEST_BOUNDED_PATH_H
#define TRAINLOG_TEST_BOUNDED_PATH_H
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
/* CONTRACT: failure leaves output empty and no caller may perform filesystem
* work with a partial path. Lengths include the separator and trailing NUL. */
static bool
trainlog_test_join_path(char *output, size_t capacity, const char *parent, const char *leaf) {
size_t parent_length;
size_t leaf_length;
if (output == NULL || capacity == 0U || parent == NULL || leaf == NULL) {
return false;
}
output[0] = '\0';
parent_length = strlen(parent);
leaf_length = strlen(leaf);
if (leaf_length > SIZE_MAX - 2U || parent_length > SIZE_MAX - leaf_length - 2U ||
parent_length + leaf_length + 2U > capacity) {
return false;
}
(void)memcpy(output, parent, parent_length);
output[parent_length] = '/';
(void)memcpy(output + parent_length + 1U, leaf, leaf_length);
output[parent_length + leaf_length + 1U] = '\0';
return true;
}
#endif

View file

@ -1,10 +1,12 @@
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "trainlog/generation_mtp.h"
#include "bounded_path.h"
typedef struct Node {
uint32_t id, parent;
@ -15,13 +17,41 @@ typedef struct Node {
static Node nodes[2048];
static size_t node_count;
static const char *remote;
static bool scan_failed;
static uint32_t stable_path_id(const char *path) {
const unsigned char *cursor = (const unsigned char *)path;
uint32_t value = UINT32_C(2166136261);
while (*cursor != '\0') {
value ^= (uint32_t)*cursor++;
value *= UINT32_C(16777619);
}
return value == 0U || value == UINT32_MAX ? value ^ UINT32_C(0x80000000) : value;
}
static uint32_t scan(const char *path, uint32_t parent) {
DIR *d;
struct dirent *e;
uint32_t self = (uint32_t)(node_count + 1U);
Node *n = &nodes[node_count++];
uint32_t self;
Node *n;
struct stat st;
lstat(path, &st);
if (lstat(path, &st) != 0) {
if (errno != ENOENT) {
scan_failed = true;
}
return 0U;
}
if (node_count >= sizeof(nodes) / sizeof(nodes[0])) {
scan_failed = true;
return 0U;
}
self = stable_path_id(path);
for (size_t index = 0U; index < node_count; index++) {
if (nodes[index].id == self && strcmp(nodes[index].path, path) != 0) {
scan_failed = true;
return 0U;
}
}
n = &nodes[node_count++];
n->id = self;
n->parent = parent;
n->folder = S_ISDIR(st.st_mode);
@ -33,17 +63,23 @@ static uint32_t scan(const char *path, uint32_t parent) {
while ((e = readdir(d))) {
if (strcmp(e->d_name, ".") && strcmp(e->d_name, "..")) {
char child[1024];
snprintf(child, sizeof(child), "%s/%s", path, e->d_name);
if (!trainlog_test_join_path(child, sizeof(child), path, e->d_name)) {
scan_failed = true;
break;
}
scan(child, self);
}
}
closedir(d);
} else if (n->folder && errno != ENOENT) {
scan_failed = true;
}
return self;
}
static void rebuild(void) {
static bool rebuild(void) {
node_count = 0U;
scan(remote, UINT32_MAX);
scan_failed = false;
return scan(remote, UINT32_MAX) != 0U && !scan_failed;
}
static Node *node(uint32_t id) {
size_t i;
@ -87,7 +123,9 @@ static TrainlogStatus children(unsigned int b,
(void)b;
(void)d;
(void)s;
rebuild();
if (!rebuild()) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
if (p == UINT32_MAX) {
p = nodes[0].id;
}
@ -121,12 +159,16 @@ static TrainlogStatus ensure(unsigned int b,
(void)b;
(void)d;
(void)s;
rebuild();
if (!rebuild()) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
parent = node(p);
if (!parent) {
return TRAINLOG_STATUS_NOT_FOUND;
}
snprintf(path, sizeof(path), "%s/%s", parent->path, name);
if (!trainlog_test_join_path(path, sizeof(path), parent->path, name)) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
if (mkdir(path, 0700) == 0) {
*created = true;
} else if (access(path, F_OK) == 0) {
@ -134,7 +176,9 @@ static TrainlogStatus ensure(unsigned int b,
} else {
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
rebuild();
if (!rebuild()) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
size_t i;
for (i = 0; i < node_count; i++) {
if (strcmp(nodes[i].path, path) == 0) {
@ -162,7 +206,9 @@ static TrainlogStatus receive(unsigned int b, unsigned int d, uint32_t id, const
Node *n;
(void)b;
(void)d;
rebuild();
if (!rebuild()) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
n = node(id);
return n && copy(n->path, path) == 0 ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_SYSTEM_ERROR;
}
@ -178,16 +224,22 @@ static TrainlogStatus send_file(unsigned int b,
(void)b;
(void)d;
(void)s;
rebuild();
if (!rebuild()) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
parent = node(p);
if (!parent) {
return TRAINLOG_STATUS_NOT_FOUND;
}
snprintf(path, sizeof(path), "%s/%s", parent->path, name);
if (!trainlog_test_join_path(path, sizeof(path), parent->path, name)) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
if (copy(local, path) != 0) {
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
rebuild();
if (!rebuild()) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
size_t i;
for (i = 0; i < node_count; i++) {
if (strcmp(nodes[i].path, path) == 0) {
@ -201,26 +253,38 @@ static TrainlogStatus remove_object(unsigned int b, unsigned int d, uint32_t id)
Node *n;
(void)b;
(void)d;
rebuild();
if (!rebuild()) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
n = node(id);
return n && unlink(n->path) == 0 ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_SYSTEM_ERROR;
}
static TrainlogStatus rename_object(unsigned int b, unsigned int d, uint32_t id, const char *name) {
Node *n;
char path[1024], *slash;
char parent[1024];
char path[1024];
char *slash;
(void)b;
(void)d;
rebuild();
if (!rebuild()) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
n = node(id);
if (!n) {
return TRAINLOG_STATUS_NOT_FOUND;
}
snprintf(path, sizeof(path), "%s", n->path);
slash = strrchr(path, '/');
if (strlen(n->path) >= sizeof(parent)) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
(void)memcpy(parent, n->path, strlen(n->path) + 1U);
slash = strrchr(parent, '/');
if (!slash) {
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
snprintf(slash + 1, (size_t)(path + sizeof(path) - (slash + 1)), "%s", name);
*slash = '\0';
if (!trainlog_test_join_path(path, sizeof(path), parent, name)) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
return rename(n->path, path) == 0 ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_SYSTEM_ERROR;
}
static const TrainlogGenerationMtpIo io = {

View file

@ -4,6 +4,7 @@
#include <sys/stat.h>
#include <unistd.h>
#include "trainlog/generation_mtp.h"
#include "bounded_path.h"
#define CHECK(x) \
do { \
@ -239,21 +240,22 @@ static int test_pull_and_manifest_last(void) {
CHECK(trainlog_generation_mtp_pull(
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag)) ==
TRAINLOG_STATUS_OK);
snprintf(path,
sizeof(path),
"%s/android-objects/generations/gen_11111111-1111-4111-8111-111111111111/payload.json",
root);
CHECK(trainlog_test_join_path(
path,
sizeof(path),
root,
"android-objects/generations/gen_11111111-1111-4111-8111-111111111111/payload.json"));
CHECK(access(path, R_OK) == 0);
snprintf(path, sizeof(path), "%s/out", root);
CHECK(trainlog_test_join_path(path, sizeof(path), root, "out"));
CHECK(mkdir(path, 0700) == 0);
char gen[512];
snprintf(gen, sizeof(gen), "%s/generation", path);
CHECK(trainlog_test_join_path(gen, sizeof(gen), path, "generation"));
CHECK(mkdir(gen, 0700) == 0);
char artifact[512];
snprintf(artifact, sizeof(artifact), "%s/artifact.json", gen);
CHECK(trainlog_test_join_path(artifact, sizeof(artifact), gen, "artifact.json"));
CHECK(write_file(artifact, "a") == 0);
char marker[512];
snprintf(marker, sizeof(marker), "%s/manifest.json", gen);
CHECK(trainlog_test_join_path(marker, sizeof(marker), gen, "manifest.json"));
CHECK(write_file(marker, "m") == 0);
CHECK(trainlog_generation_mtp_push(
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag)) ==
@ -290,11 +292,36 @@ static int test_ambiguity_and_truncation(void) {
status = trainlog_generation_mtp_pull(
&io, "peer_11111111-1111-4111-8111-111111111111", root, diag, sizeof(diag));
CHECK(status != TRAINLOG_STATUS_OK);
snprintf(path, sizeof(path), "%s/bad.bin", root);
CHECK(trainlog_test_join_path(path, sizeof(path), root, "bad.bin"));
CHECK(access(path, F_OK) != 0);
return 0;
}
static int test_bounded_path_join(void) {
char exact[8];
char too_small[7] = "guard";
char multibyte[16];
char private_root[] = "/tmp/trainlog-path-bound-XXXXXX";
char sentinel[128];
char refused[8] = "guard";
CHECK(trainlog_test_join_path(exact, sizeof(exact), "abc", "def"));
CHECK(strcmp(exact, "abc/def") == 0);
CHECK(!trainlog_test_join_path(too_small, sizeof(too_small), "abc", "def"));
CHECK(too_small[0] == '\0');
CHECK(trainlog_test_join_path(multibyte, sizeof(multibyte), "rép", "é"));
CHECK(strcmp(multibyte, "rép/é") == 0);
CHECK(mkdtemp(private_root) != NULL);
CHECK(trainlog_test_join_path(sentinel, sizeof(sentinel), private_root, "sentinel"));
CHECK(write_file(sentinel, "outside") == 0);
CHECK(!trainlog_test_join_path(refused, sizeof(refused), private_root, "overflow"));
CHECK(refused[0] == '\0');
CHECK(access(sentinel, R_OK) == 0);
CHECK(unlink(sentinel) == 0);
CHECK(rmdir(private_root) == 0);
return 0;
}
int main(void) {
CHECK(test_bounded_path_join() == 0);
CHECK(test_pull_and_manifest_last() == 0);
CHECK(test_ambiguity_and_truncation() == 0);
return 0;

View file

@ -67,7 +67,7 @@ static bool run_test(void)
char path[] = "/tmp/trainlog-v16-XXXXXX";
int fd = mkstemp(path); sqlite3 *raw = NULL; TrainlogDatabase *db = NULL;
bool ok = false;
TrainlogPersistedExerciseDetail old[1] = {{0}};
TrainlogPersistedExerciseDetail old[1];
TrainlogSessionSummary summary;
size_t count = 0U; int version = 0;
TrainlogExercise changed;
@ -75,6 +75,7 @@ static bool run_test(void)
TrainlogSessionExerciseInput input = {0};
TrainlogSessionInput session = {0};
char diagnostic[512] = {0};
(void)memset(old, 0, sizeof(old));
CHECK(fd >= 0); CHECK(close(fd) == 0); fd = -1;
CHECK(sqlite3_open(path, &raw) == SQLITE_OK);
CHECK(sqlite3_exec(raw, FIXTURE, NULL, NULL, NULL) == SQLITE_OK);

View file

@ -149,6 +149,31 @@ static bool response_header(const char *response, const char *name, char *output
return true;
}
static bool test_sync_accepted_serialization(void) {
static const char REQUEST_ID[] = "sy_11111111-1111-4111-8111-111111111111";
static const char RUN_ID[] = "sy_22222222-2222-4222-8222-222222222222";
char output[256];
char exact[256];
char too_small[256];
size_t output_size;
CHECK(trainlog_web_sync_accepted_serialize(
REQUEST_ID, RUN_ID, output, sizeof(output), &output_size));
CHECK(output_size == strlen(output));
CHECK(strstr(output, "\"request_id\":\"sy_11111111-1111-4111-8111-111111111111\"") != NULL);
CHECK(strstr(output, "\"run_id\":\"sy_22222222-2222-4222-8222-222222222222\"") != NULL);
CHECK(strstr(output, "\"result\":\"running\"") != NULL);
CHECK(trainlog_web_sync_accepted_serialize(
REQUEST_ID, RUN_ID, exact, output_size + 1U, &output_size));
CHECK(strcmp(exact, output) == 0);
CHECK(!trainlog_web_sync_accepted_serialize(
REQUEST_ID, RUN_ID, too_small, output_size, &output_size));
CHECK(too_small[0] == '\0');
CHECK(!trainlog_web_sync_accepted_serialize(
"sy_invalid", RUN_ID, output, sizeof(output), &output_size));
return true;
}
static bool test_http_contract(TrainlogDatabase *database) {
char response[32768];
char large_request[12000];
@ -476,8 +501,8 @@ int main(void) {
sizeof(diagnostic)) != 0 &&
strstr(diagnostic, "frontend") != NULL;
} else {
passed = test_http_contract(database) && test_port_in_use(database) &&
test_dashboard_core_error_translation();
passed = test_sync_accepted_serialization() && test_http_contract(database) &&
test_port_in_use(database) && test_dashboard_core_error_translation();
}
trainlog_database_close(database);
return passed ? 0 : 1;