fix(sync): reject truncated native artifacts
This commit is contained in:
parent
d29532a735
commit
2c5a8be282
9 changed files with 854 additions and 446 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
1053
tui/src/web_server.c
1053
tui/src/web_server.c
File diff suppressed because it is too large
Load diff
33
tui/tests/bounded_path.h
Normal file
33
tui/tests/bounded_path.h
Normal 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
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue