feat(detect): implement check registry probe specs

This commit is contained in:
2026-08-28 22:30:53 -04:00
parent 00f4f0e1a0
commit 7a8abea6d9
3 changed files with 1382 additions and 0 deletions
+482
View File
@@ -0,0 +1,482 @@
/* LINK: ../../src/detect/checks.c ../../src/detect/check_registry.c ../../src/kdl/schema.c ../../src/kdl/parser.c ../../src/kdl/lexer.c ../../src/kdl/value.c ../../src/error.c ../../src/span.c */
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L /* popen/pclose, sys/wait.h */
#endif
/*
* tests/unit/test_checks.c
*
* Unit tests for the check registry implementation (todo 11): for each of
* the 8 check kinds, src/detect/checks.c builds the exact PROBE SPEC
* (mode + C snippet + command argv + extra args) that todo 12 turns into
* configure-time compile/link/run/command invocations. This module builds
* specs ONLY - it never executes a probe and never emits shell text.
*
* The magic LINK comment on line 1 is REQUIRED by tests/run.sh: it lists
* the extra .c sources compiled into this test binary (paths relative to
* tests/unit/). checks.c needs check_registry.c (the shape table) + value.c
* (arg extraction); parser.c + lexer.c build documents end-to-end from
* source text; error.c + span.c via error.c's st_span_print; schema.c
* linked per the task's file list.
*/
#include "munit.h"
#include "detect/check_registry.h"
#include "detect/checks.h"
#include "error.h"
#include "kdl/ast.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
/* ---- helpers ---------------------------------------------------------- */
/* Parse a single-node document and return its one node (parse success is
* asserted). The caller frees the returned document. */
static struct st_kdl_node *
parse_one_node(const char *src, struct st_kdl_document **doc_out)
{
struct st_error *err = NULL;
*doc_out = st_kdl_parse(src, "t.kdl", &err);
munit_assert_null(err);
munit_assert_not_null(*doc_out);
return (*doc_out)->nodes;
}
/* Parse `src` as a single check node, resolve its kind, and build its
* probe spec into `*out`. Asserts parse + kind resolution + build success
* and frees the document before returning (the probe is fully heap-owned
* and independent of the document). Returns the resolved kind. */
static enum st_check_kind
build_from(const char *src, struct st_check_probe *out)
{
struct st_error *err = NULL;
struct st_kdl_document *doc = NULL;
struct st_kdl_node *node;
enum st_check_kind kind;
node = parse_one_node(src, &doc);
kind = st_check_kind_from_node(node, &err);
munit_assert_null(err);
munit_assert_int(kind, !=, ST_CHECK_KIND_COUNT);
munit_assert_null(st_check_probe_build(kind, node, out));
st_kdl_document_free(doc);
return kind;
}
/* Compile `src` through a real cc on stdin (compile-only), with an
* optional leading flag ("" for none, or e.g. "-fno-builtin "). Returns
* the compiler's exit status, or -1 on any setup failure. The cc command
* is a fixed literal; `src` reaches cc via stdin, never via the shell. */
static int
cc_compile_stdin(const char *src, const char *flag)
{
char cmd[256];
FILE *p;
int n;
int rc;
size_t len = strlen(src);
n = snprintf(cmd, sizeof cmd,
"cc -std=c23 -Wall -Wextra -Wpedantic %s-c -x c - -o /dev/null",
flag != NULL ? flag : "");
if (n < 0 || (size_t)n >= sizeof cmd) {
return -1;
}
p = popen(cmd, "w");
if (p == NULL) {
return -1;
}
if (fwrite(src, 1, len, p) != len) {
(void)pclose(p);
return -1;
}
rc = pclose(p);
if (rc == -1 || !WIFEXITED(rc)) {
return -1;
}
return WEXITSTATUS(rc);
}
/* ---- tests ------------------------------------------------------------ */
/* (a) header "pthread.h" -> COMPILE probe whose source includes the
* header with the angle-bracket form. */
static MunitResult
test_header(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_check_probe p = { 0 };
enum st_check_kind kind = build_from("header \"pthread.h\"", &p);
munit_assert_int(kind, ==, ST_CHECK_HEADER);
munit_assert_int(p.mode, ==, ST_PROBE_COMPILE);
munit_assert_not_null(p.c_source);
munit_assert_true(strstr(p.c_source, "#include <pthread.h>") != NULL);
munit_assert_null(p.command);
munit_assert_null(p.extra_args);
munit_assert_null(p.version_constraint);
st_check_probe_free(&p);
return MUNIT_OK;
}
/* (b) function "strdup" -> LINK probe whose source declares and calls the
* symbol; -fno-builtin so GCC's builtin machinery can't reject/mis-type
* the declaration. */
static MunitResult
test_function(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_check_probe p = { 0 };
enum st_check_kind kind = build_from("function \"strdup\"", &p);
munit_assert_int(kind, ==, ST_CHECK_FUNCTION);
munit_assert_int(p.mode, ==, ST_PROBE_LINK);
munit_assert_not_null(p.c_source);
munit_assert_true(strstr(p.c_source, "extern void strdup(void)") != NULL);
munit_assert_true(strstr(p.c_source, "strdup();") != NULL);
munit_assert_not_null(p.extra_args);
munit_assert_string_equal(p.extra_args[0], "-fno-builtin");
munit_assert_null(p.extra_args[1]);
st_check_probe_free(&p);
return MUNIT_OK;
}
/* (c) library "pthread" -> LINK probe with extra_arg "-lpthread". */
static MunitResult
test_library(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_check_probe p = { 0 };
enum st_check_kind kind = build_from("library \"pthread\"", &p);
munit_assert_int(kind, ==, ST_CHECK_LIBRARY);
munit_assert_int(p.mode, ==, ST_PROBE_LINK);
munit_assert_not_null(p.c_source);
munit_assert_not_null(p.extra_args);
munit_assert_string_equal(p.extra_args[0], "-lpthread");
munit_assert_null(p.extra_args[1]);
munit_assert_null(p.version_constraint);
st_check_probe_free(&p);
return MUNIT_OK;
}
/* (d) type "size_t" -> COMPILE probe using sizeof(size_t) in a static
* assertion. */
static MunitResult
test_type(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_check_probe p = { 0 };
enum st_check_kind kind = build_from("type \"size_t\"", &p);
munit_assert_int(kind, ==, ST_CHECK_TYPE);
munit_assert_int(p.mode, ==, ST_PROBE_COMPILE);
munit_assert_not_null(p.c_source);
munit_assert_true(strstr(p.c_source, "sizeof(size_t)") != NULL);
munit_assert_true(strstr(p.c_source, "_Static_assert") != NULL);
st_check_probe_free(&p);
return MUNIT_OK;
}
/* (e) sizeof "long" -> RUN-capable probe: a _Static_assert sentinel plus a
* printf run path. */
static MunitResult
test_sizeof(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_check_probe p = { 0 };
enum st_check_kind kind = build_from("sizeof \"long\"", &p);
munit_assert_int(kind, ==, ST_CHECK_SIZEOF);
munit_assert_int(p.mode, ==, ST_PROBE_RUN);
munit_assert_not_null(p.c_source);
munit_assert_true(strstr(p.c_source, "sizeof(long)") != NULL);
munit_assert_true(strstr(p.c_source, "_Static_assert") != NULL);
munit_assert_true(strstr(p.c_source, "printf") != NULL);
st_check_probe_free(&p);
return MUNIT_OK;
}
/* (f) program "pkg-config" -> COMMAND argv {command, -v, pkg-config}. */
static MunitResult
test_program(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_check_probe p = { 0 };
enum st_check_kind kind = build_from("program \"pkg-config\"", &p);
munit_assert_int(kind, ==, ST_CHECK_PROGRAM);
munit_assert_int(p.mode, ==, ST_PROBE_COMMAND);
munit_assert_null(p.c_source);
munit_assert_not_null(p.command);
munit_assert_string_equal(p.command[0], "command");
munit_assert_string_equal(p.command[1], "-v");
munit_assert_string_equal(p.command[2], "pkg-config");
munit_assert_null(p.command[3]);
st_check_probe_free(&p);
return MUNIT_OK;
}
/* (g) compiler_flag "-fsanitize=address" -> COMPILE probe with the flag as
* an extra_arg. */
static MunitResult
test_compiler_flag(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_check_probe p = { 0 };
enum st_check_kind kind = build_from("compiler_flag \"-fsanitize=address\"",
&p);
munit_assert_int(kind, ==, ST_CHECK_COMPILER_FLAG);
munit_assert_int(p.mode, ==, ST_PROBE_COMPILE);
munit_assert_not_null(p.c_source);
munit_assert_not_null(p.extra_args);
munit_assert_string_equal(p.extra_args[0], "-fsanitize=address");
munit_assert_null(p.extra_args[1]);
st_check_probe_free(&p);
return MUNIT_OK;
}
/* (h) pkg_config "openssl" -> COMMAND argv {pkg-config, --cflags, --libs,
* openssl}. */
static MunitResult
test_pkg_config(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_check_probe p = { 0 };
enum st_check_kind kind = build_from("pkg_config \"openssl\"", &p);
munit_assert_int(kind, ==, ST_CHECK_PKG_CONFIG);
munit_assert_int(p.mode, ==, ST_PROBE_COMMAND);
munit_assert_null(p.c_source);
munit_assert_not_null(p.command);
munit_assert_string_equal(p.command[0], "pkg-config");
munit_assert_string_equal(p.command[1], "--cflags");
munit_assert_string_equal(p.command[2], "--libs");
munit_assert_string_equal(p.command[3], "openssl");
munit_assert_null(p.command[4]);
munit_assert_null(p.version_constraint);
st_check_probe_free(&p);
return MUNIT_OK;
}
/* The optional `version` constraint: library records it (and still links
* with plain -l); pkg_config maps >= to --atleast-version=. */
static MunitResult
test_version_constraints(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_check_probe lib = { 0 };
struct st_check_probe pk = { 0 };
struct st_check_probe pkeq = { 0 };
(void)build_from("library \"pthread\" version \">=2.0\"", &lib);
munit_assert_int(lib.mode, ==, ST_PROBE_LINK);
munit_assert_string_equal(lib.extra_args[0], "-lpthread");
munit_assert_string_equal(lib.version_constraint, ">=2.0");
(void)build_from("pkg_config \"openssl\" version \">=1.1\"", &pk);
munit_assert_int(pk.mode, ==, ST_PROBE_COMMAND);
munit_assert_string_equal(pk.command[0], "pkg-config");
munit_assert_string_equal(pk.command[1], "--atleast-version=1.1");
munit_assert_string_equal(pk.command[2], "--cflags");
munit_assert_string_equal(pk.command[3], "--libs");
munit_assert_string_equal(pk.command[4], "openssl");
munit_assert_null(pk.command[5]);
munit_assert_string_equal(pk.version_constraint, ">=1.1");
(void)build_from("pkg_config \"openssl\" version \"=1.2\"", &pkeq);
munit_assert_string_equal(pkeq.command[1], "--exact-version=1.2");
st_check_probe_free(&lib);
st_check_probe_free(&pk);
st_check_probe_free(&pkeq);
return MUNIT_OK;
}
/* A malformed version constraint (no operator) is rejected with a schema
* error naming the check. */
static MunitResult
test_malformed_constraint(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_error *err = NULL;
struct st_kdl_document *doc = NULL;
struct st_kdl_node *node;
struct st_check_probe p = { 0 };
node = parse_one_node("library \"l\" version \"7.0\"", &doc);
err = st_check_probe_build(ST_CHECK_LIBRARY, node, &p);
munit_assert_not_null(err);
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
munit_assert_true(strstr(st_error_message(err), "malformed version "
"constraint") != NULL);
st_error_free(err);
st_kdl_document_free(doc);
return MUNIT_OK;
}
/* (j) unknown kind / NULL node / missing argument all error cleanly. */
static MunitResult
test_bad_input(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_error *err = NULL;
struct st_check_probe p = { 0 };
struct st_kdl_document *doc = NULL;
struct st_kdl_node *node;
err = st_check_probe_build(ST_CHECK_KIND_COUNT, NULL, &p);
munit_assert_not_null(err);
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
st_error_free(err);
err = st_check_probe_build(ST_CHECK_HEADER, NULL, &p);
munit_assert_not_null(err);
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
st_error_free(err);
node = parse_one_node("header", &doc);
err = st_check_probe_build(ST_CHECK_HEADER, node, &p);
munit_assert_not_null(err);
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
munit_assert_true(strstr(st_error_message(err), "requires an argument")
!= NULL);
st_error_free(err);
st_kdl_document_free(doc);
return MUNIT_OK;
}
/* (i) COMPILE-PROOF: three generated snippets actually compile under
* `cc -std=c23 -Wall -Wextra -Wpedantic -c -x c -` (exit 0); the function
* snippet additionally compiles clean with its own -fno-builtin arg. */
static MunitResult
test_compile_proof(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_check_probe hdr = { 0 };
struct st_check_probe lib = { 0 };
struct st_check_probe sz = { 0 };
struct st_check_probe fn = { 0 };
(void)build_from("header \"pthread.h\"", &hdr);
(void)build_from("library \"pthread\"", &lib);
(void)build_from("sizeof \"long\"", &sz);
(void)build_from("function \"strdup\"", &fn);
munit_assert_int(cc_compile_stdin(hdr.c_source, ""), ==, 0);
munit_assert_int(cc_compile_stdin(lib.c_source, ""), ==, 0);
munit_assert_int(cc_compile_stdin(sz.c_source, ""), ==, 0);
munit_assert_int(cc_compile_stdin(fn.c_source, "-fno-builtin "), ==, 0);
st_check_probe_free(&hdr);
st_check_probe_free(&lib);
st_check_probe_free(&sz);
st_check_probe_free(&fn);
return MUNIT_OK;
}
/* Adversarial: a header name with metacharacters yields a single-line
* quoted include (no injection, valid C); a non-identifier function name
* errors; a COMMAND probe carries its program name as ONE argv element. */
static MunitResult
test_adversarial(const MunitParameter params[], void *data)
{
(void)params;
(void)data;
struct st_check_probe p = { 0 };
struct st_error *err = NULL;
struct st_kdl_document *doc = NULL;
size_t len;
(void)build_from("header \"\\\"; rm -rf /\"", &p);
munit_assert_int(p.mode, ==, ST_PROBE_COMPILE);
munit_assert_true(strncmp(p.c_source, "#include \"", 10) == 0);
munit_assert_true(strstr(p.c_source, "rm -rf /") != NULL);
munit_assert_null(strchr(p.c_source, '<'));
len = strlen(p.c_source);
munit_assert_true(len >= 1);
munit_assert_true(p.c_source[len - 1] == '\n');
munit_assert_true(strchr(p.c_source, '\n') == &p.c_source[len - 1]);
st_check_probe_free(&p);
doc = st_kdl_parse("function \"foo bar\"", "t.kdl", &err);
munit_assert_not_null(doc);
munit_assert_null(err);
err = st_check_probe_build(ST_CHECK_FUNCTION, doc->nodes, &p);
munit_assert_not_null(err);
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
munit_assert_true(strstr(st_error_message(err), "not a valid C "
"identifier") != NULL);
st_error_free(err);
st_kdl_document_free(doc);
doc = NULL;
err = NULL;
doc = st_kdl_parse("program \"pkg-config; rm -rf /\"", "t.kdl", &err);
munit_assert_not_null(doc);
munit_assert_null(err);
munit_assert_null(st_check_probe_build(ST_CHECK_PROGRAM, doc->nodes, &p));
munit_assert_int(p.mode, ==, ST_PROBE_COMMAND);
munit_assert_string_equal(p.command[2], "pkg-config; rm -rf /");
munit_assert_null(p.command[3]);
st_check_probe_free(&p);
st_kdl_document_free(doc);
return MUNIT_OK;
}
static MunitTest tests[] = {
{ "/checks/header", test_header, NULL, NULL, MUNIT_TEST_OPTION_NONE,
NULL },
{ "/checks/function", test_function, NULL, NULL, MUNIT_TEST_OPTION_NONE,
NULL },
{ "/checks/library", test_library, NULL, NULL, MUNIT_TEST_OPTION_NONE,
NULL },
{ "/checks/type", test_type, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
{ "/checks/sizeof", test_sizeof, NULL, NULL, MUNIT_TEST_OPTION_NONE,
NULL },
{ "/checks/program", test_program, NULL, NULL, MUNIT_TEST_OPTION_NONE,
NULL },
{ "/checks/compiler-flag", test_compiler_flag, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/checks/pkg-config", test_pkg_config, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/checks/version-constraints", test_version_constraints, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/checks/malformed-constraint", test_malformed_constraint, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/checks/bad-input", test_bad_input, NULL, NULL, MUNIT_TEST_OPTION_NONE,
NULL },
{ "/checks/compile-proof", test_compile_proof, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ "/checks/adversarial", test_adversarial, NULL, NULL,
MUNIT_TEST_OPTION_NONE, NULL },
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
};
static const MunitSuite suite = {
"/checks", tests, NULL, 1, MUNIT_SUITE_OPTION_NONE,
};
int
main(int argc, char *argv[MUNIT_ARRAY_PARAM(argc + 1)])
{
return munit_suite_main(&suite, NULL, argc, argv);
}