/* 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 #include #include #include /* ---- 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 ") != 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); }