Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added diagnostic/build-f522e200-part001.logd
Binary file not shown.
Binary file added diagnostic/build-f522e200-part002.logd
Binary file not shown.
Binary file added diagnostic/build-f522e200-part003.logd
Binary file not shown.
Binary file added diagnostic/build-f522e200-part004.logd
Binary file not shown.
Binary file added diagnostic/build-f522e200-part005.logd
Binary file not shown.
93 changes: 93 additions & 0 deletions diagnostic/build-f522e200.json

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,29 @@ The health check returns a 200 OK response with a JSON body:
}
```

### Frailbox Self-Test Summary

The frailbox runtime includes a deterministic, non-networked self-test mode for
CI and operator checks. The default text output is intended for terminal use:

```sh
cd frailbox
make test
```

CI jobs and automation can request a machine-readable JSON summary:

```sh
cd frailbox
make test-selftest-json
./frailbox --self-test --self-test-format json
```

The JSON payload includes a top-level `summary` object and a `tests` array. Each
test entry contains `name`, `status`, and `duration_ms`; failed entries also
include `failure_reason` when the failure source is known. A passing run exits
with status 0, while any failed self-test exits non-zero.

### Prometheus Metrics

Each service exposes Prometheus metrics at `/metrics` on the same port as the
Expand Down
7 changes: 5 additions & 2 deletions frailbox/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,12 @@ distclean: clean
rm -rf *.o *.d

test: $(TARGET)
./$(TARGET) --sandbox-type seccomp --memory-limit 64 --verbose
./$(TARGET) --self-test --self-test-format text

test-selftest-json: $(TARGET)
./tests/test_selftest_json.sh ./$(TARGET)

valgrind: $(TARGET)
valgrind --leak-check=full --show-leak-kinds=all ./$(TARGET)

.PHONY: all clean distclean test valgrind
.PHONY: all clean distclean test test-selftest-json valgrind
277 changes: 277 additions & 0 deletions frailbox/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,31 @@

static volatile sig_atomic_t running = 1;

typedef enum self_test_format {
SELF_TEST_TEXT = 0,
SELF_TEST_JSON = 1,
} self_test_format_t;

typedef struct self_test_result {
const char *name;
int passed;
double duration_ms;
char failure_reason[160];
} self_test_result_t;

typedef int (*self_test_fn)(const char *inject_failure, char *reason, size_t reason_size);

typedef struct self_test_case {
const char *name;
self_test_fn run;
} self_test_case_t;

enum {
OPT_SELF_TEST = 1000,
OPT_SELF_TEST_FORMAT,
OPT_SELF_TEST_INJECT_FAILURE,
};

static void handle_signal(int sig) {
(void)sig;
running = 0;
Expand Down Expand Up @@ -57,9 +82,236 @@ static void print_config(const sandbox_config_t *config) {
fprintf(stdout, "\n");
}

static double elapsed_ms(const struct timespec *start, const struct timespec *end) {
time_t sec = end->tv_sec - start->tv_sec;
long nsec = end->tv_nsec - start->tv_nsec;

if (nsec < 0) {
sec--;
nsec += 1000000000L;
}

return ((double)sec * 1000.0) + ((double)nsec / 1000000.0);
}

static int should_inject_failure(const char *inject_failure, const char *test_name) {
return inject_failure && strcmp(inject_failure, test_name) == 0;
}

static int fail_reason(char *reason, size_t reason_size, const char *message) {
snprintf(reason, reason_size, "%s", message);
return 0;
}

static int self_test_arena_allocator(
const char *inject_failure, char *reason, size_t reason_size) {
if (should_inject_failure(inject_failure, "arena_allocator")) {
return fail_reason(reason, reason_size,
"fixture failure requested for arena_allocator");
}

arena_t *arena = arena_create(4096, ARENA_ZERO_INIT);
if (!arena) {
return fail_reason(reason, reason_size, "arena_create returned NULL");
}

unsigned char *bytes = arena_alloc(arena, 64);
unsigned char *zeroed = arena_calloc(arena, 16, 2);
void *aligned = arena_alloc_aligned(arena, 128, 64);

if (!bytes || !zeroed || !aligned) {
arena_destroy(arena);
return fail_reason(reason, reason_size, "arena allocation returned NULL");
}

for (size_t i = 0; i < 32; i++) {
if (zeroed[i] != 0) {
arena_destroy(arena);
return fail_reason(reason, reason_size, "arena_calloc returned non-zero data");
}
}

arena_stats_t stats = arena_get_stats(arena);
if (stats.allocation_count != 3) {
arena_destroy(arena);
return fail_reason(reason, reason_size, "allocation count did not match fixture");
}
if (!arena_contains(arena, bytes) || arena_total_capacity(arena) < 4096) {
arena_destroy(arena);
return fail_reason(reason, reason_size, "arena ownership/capacity check failed");
}

arena_destroy(arena);
return 1;
}

static int self_test_sandbox_config(
const char *inject_failure, char *reason, size_t reason_size) {
if (should_inject_failure(inject_failure, "sandbox_config")) {
return fail_reason(reason, reason_size,
"fixture failure requested for sandbox_config");
}

sandbox_config_t config;
memset(&config, 0, sizeof(config));
config.type = SANDBOX_NONE;
config.memory_limit_bytes = 64ULL * 1024ULL * 1024ULL;
config.cpu_limit_ns = 1000ULL * 1000000ULL;
config.max_processes = 10;
config.max_open_fds = 64;

sandbox_t *sandbox = sandbox_create(&config);
if (!sandbox) {
return fail_reason(reason, reason_size, "sandbox_create returned NULL");
}

if (sandbox_add_rule(sandbox, CAP_FILE_READ, ACTION_ALLOW) != 0) {
sandbox_destroy(sandbox);
return fail_reason(reason, reason_size, "sandbox_add_rule failed");
}

if (sandbox->config.rule_count != 1) {
sandbox_destroy(sandbox);
return fail_reason(reason, reason_size, "sandbox rule count did not match fixture");
}

if (sandbox_apply(sandbox) != 0 || !sandbox_is_active(sandbox)) {
sandbox_destroy(sandbox);
return fail_reason(reason, reason_size, "SANDBOX_NONE did not become active");
}

sandbox_destroy(sandbox);
return 1;
}

static void json_string(FILE *out, const char *value) {
fputc('"', out);
for (const unsigned char *p = (const unsigned char *)value; *p; p++) {
switch (*p) {
case '"':
fputs("\\\"", out);
break;
case '\\':
fputs("\\\\", out);
break;
case '\b':
fputs("\\b", out);
break;
case '\f':
fputs("\\f", out);
break;
case '\n':
fputs("\\n", out);
break;
case '\r':
fputs("\\r", out);
break;
case '\t':
fputs("\\t", out);
break;
default:
if (*p < 0x20) {
fprintf(out, "\\u%04x", *p);
} else {
fputc(*p, out);
}
break;
}
}
fputc('"', out);
}

static int run_self_tests(self_test_format_t format, const char *inject_failure) {
const self_test_case_t tests[] = {
{ "arena_allocator", self_test_arena_allocator },
{ "sandbox_config", self_test_sandbox_config },
};
const size_t test_count = sizeof(tests) / sizeof(tests[0]);
self_test_result_t results[sizeof(tests) / sizeof(tests[0])];
struct timespec suite_start;
struct timespec suite_end;
size_t passed = 0;
size_t failed = 0;

clock_gettime(CLOCK_MONOTONIC, &suite_start);

for (size_t i = 0; i < test_count; i++) {
struct timespec test_start;
struct timespec test_end;

memset(&results[i], 0, sizeof(results[i]));
results[i].name = tests[i].name;

clock_gettime(CLOCK_MONOTONIC, &test_start);
results[i].passed = tests[i].run(
inject_failure, results[i].failure_reason,
sizeof(results[i].failure_reason));
clock_gettime(CLOCK_MONOTONIC, &test_end);
results[i].duration_ms = elapsed_ms(&test_start, &test_end);

if (results[i].passed) {
passed++;
} else {
failed++;
}
}

clock_gettime(CLOCK_MONOTONIC, &suite_end);

if (format == SELF_TEST_JSON) {
fprintf(stdout, "{");
fprintf(stdout, "\"summary\":{");
fprintf(stdout, "\"status\":");
json_string(stdout, failed == 0 ? "pass" : "fail");
fprintf(stdout, ",\"total\":%zu,\"passed\":%zu,\"failed\":%zu",
test_count, passed, failed);
fprintf(stdout, ",\"duration_ms\":%.3f", elapsed_ms(&suite_start, &suite_end));
fprintf(stdout, "},\"tests\":[");

for (size_t i = 0; i < test_count; i++) {
if (i > 0) {
fputc(',', stdout);
}

fprintf(stdout, "{");
fprintf(stdout, "\"name\":");
json_string(stdout, results[i].name);
fprintf(stdout, ",\"status\":");
json_string(stdout, results[i].passed ? "pass" : "fail");
fprintf(stdout, ",\"duration_ms\":%.3f", results[i].duration_ms);
if (!results[i].passed && results[i].failure_reason[0]) {
fprintf(stdout, ",\"failure_reason\":");
json_string(stdout, results[i].failure_reason);
}
fprintf(stdout, "}");
}

fprintf(stdout, "]}\n");
return failed == 0 ? 0 : 1;
}

fprintf(stdout, "frailbox self-test: %s (%zu passed, %zu failed, %.3f ms)\n",
failed == 0 ? "PASS" : "FAIL", passed, failed,
elapsed_ms(&suite_start, &suite_end));
for (size_t i = 0; i < test_count; i++) {
fprintf(stdout, " [%s] %s (%.3f ms)",
results[i].passed ? "PASS" : "FAIL",
results[i].name, results[i].duration_ms);
if (!results[i].passed && results[i].failure_reason[0]) {
fprintf(stdout, ": %s", results[i].failure_reason);
}
fprintf(stdout, "\n");
}

return failed == 0 ? 0 : 1;
}

int main(int argc, char *argv[]) {
int opt;
int verbose = 0;
int self_test = 0;
self_test_format_t self_test_format = SELF_TEST_TEXT;
const char *self_test_inject_failure = NULL;
sandbox_type_t sandbox_type = SANDBOX_SECCOMP;
uint64_t memory_limit_mb = 256;
uint64_t cpu_limit_ms = 1000;
Expand All @@ -69,6 +321,9 @@ int main(int argc, char *argv[]) {
{"memory-limit", required_argument, 0, 'm'},
{"cpu-limit", required_argument, 0, 'c'},
{"verbose", no_argument, 0, 'v'},
{"self-test", no_argument, 0, OPT_SELF_TEST},
{"self-test-format", required_argument, 0, OPT_SELF_TEST_FORMAT},
{"self-test-inject-failure", required_argument, 0, OPT_SELF_TEST_INJECT_FAILURE},
{"help", no_argument, 0, 'h'},
{"version", no_argument, 0, 'V'},
{0, 0, 0, 0}
Expand Down Expand Up @@ -97,13 +352,31 @@ int main(int argc, char *argv[]) {
case 'v':
verbose = 1;
break;
case OPT_SELF_TEST:
self_test = 1;
break;
case OPT_SELF_TEST_FORMAT:
if (strcmp(optarg, "text") == 0)
self_test_format = SELF_TEST_TEXT;
else if (strcmp(optarg, "json") == 0)
self_test_format = SELF_TEST_JSON;
else {
fprintf(stderr, "unknown self-test format: %s\n", optarg);
return 1;
}
break;
case OPT_SELF_TEST_INJECT_FAILURE:
self_test_inject_failure = optarg;
break;
case 'h':
fprintf(stdout, "Usage: %s [options]\n\n", argv[0]);
fprintf(stdout, "Options:\n");
fprintf(stdout, " -t, --sandbox-type TYPE sandbox type (seccomp, namespace, none)\n");
fprintf(stdout, " -m, --memory-limit MB memory limit in megabytes\n");
fprintf(stdout, " -c, --cpu-limit MS CPU time limit in milliseconds\n");
fprintf(stdout, " -v, --verbose verbose output\n");
fprintf(stdout, " --self-test run deterministic self-tests and exit\n");
fprintf(stdout, " --self-test-format F self-test output format (text, json)\n");
fprintf(stdout, " -h, --help show this help\n");
fprintf(stdout, " -V, --version show version\n");
return 0;
Expand All @@ -115,6 +388,10 @@ int main(int argc, char *argv[]) {
}
}

if (self_test) {
return run_self_tests(self_test_format, self_test_inject_failure);
}

if (signal(SIGINT, handle_signal) == SIG_ERR) {
perror("signal");
return 1;
Expand Down
Loading