/* LINK: ../../src/detect/probe.c ../../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/gen/sh_emit.c ../../src/error.c ../../src/span.c */ #ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L /* mkdtemp, system/WEXITSTATUS */ #endif /* * tests/unit/test_probe.c * * Unit tests for the configure-time probe snippet generator (todo 12): * src/detect/probe.c. THE MODEL: probes do NOT run here, at stupidtools * generation time — this module only EMITS POSIX-sh that runs at configure * time inside the generated ./configure. These tests prove that emitted * shell is syntactically valid (sh/bash/zsh -n), genuinely executes (real * `cc` against real headers), short-circuits on config.cache, cross-compile * guards RUN probes, and keeps hostile probe targets inert via st_sh_quote. * * The magic LINK comment on line 1 is REQUIRED by tests/run.sh (extra .c * sources, relative to tests/unit/). probe.c needs sh_emit.c (st_sh_quote) * + error.c + span.c; checks.c/check_registry.c/parser.c/lexer.c/value.c/ * schema.c are linked so canonical probes are built END-TO-END from DSL * source (st_check_probe_build), exercising the real checks.c -> probe.c * contract rather than hand-built structs alone. */ #include "munit.h" #include "detect/check_registry.h" #include "detect/checks.h" #include "detect/probe.h" #include "error.h" #include "kdl/ast.h" #include #include #include #include #include #include /* ---- per-test temp dir ------------------------------------------------- */ static char temp_dir[128]; static void * setup(const MunitParameter params[], void *user_data) { (void)params; (void)user_data; int n = snprintf(temp_dir, sizeof temp_dir, "/tmp/st_probe_XXXXXX"); if (n < 0) { return NULL; } if (mkdtemp(temp_dir) == NULL) { return NULL; } return (void *)1; } static void teardown(void *fixture) { char cmd[160]; int n; if (fixture == NULL || temp_dir[0] == '\0') { return; } n = snprintf(cmd, sizeof cmd, "rm -rf -- '%s'", temp_dir); if (n < 0 || (size_t)n >= sizeof cmd) { return; } (void)system(cmd); /* best-effort; mkdtemp names are [A-Za-z0-9_]* */ temp_dir[0] = '\0'; } /* Join temp_dir/`name` into `buf`; returns the snprintf result so callers * can assert it without ever stringifying a %-carrying format (the munit * %s-stringification trap, see .omo/notepads/stupidtools/learnings.md). */ static int mkpath(char *buf, size_t sz, const char *name) { return snprintf(buf, sz, "%s/%s", temp_dir, name); } /* ---- probe construction ------------------------------------------------- */ /* Parse `src` as one check node, resolve its kind, build its probe spec * into `*out` (fully heap-owned). Asserts each step succeeds and frees the * document before returning. The caller frees via st_check_probe_free. */ 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; doc = st_kdl_parse(src, "t.kdl", &err); munit_assert_null(err); munit_assert_not_null(doc); node = doc->nodes; 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; } /* ---- emit helpers ------------------------------------------------------ */ /* Emit one check's snippet into a temp file, return its bytes (NUL- * terminated). Returns NULL on any failure. The buffer is static (munit * forks per test, so it is never shared across tests). */ static const char * emit_snippet_text(const char *checkname, const struct st_check_probe *p) { static char buf[8192]; char path[600]; FILE *f; size_t n; struct st_error *err; int m; m = mkpath(path, sizeof path, "snippet.sh"); if (m < 0) { return NULL; } f = fopen(path, "w"); if (f == NULL) { return NULL; } err = st_probe_emit_snippet(checkname, p, f); fclose(f); if (err != NULL) { st_error_free(err); return NULL; } f = fopen(path, "r"); if (f == NULL) { return NULL; } n = fread(buf, 1, sizeof buf - 1, f); fclose(f); buf[n] = '\0'; return buf; } /* The configure-time environment the script's stub preamble plants. */ struct env { const char *cc; /* CC value; NULL -> "cc" */ const char *cc_id; /* st_cc_id; NULL -> "gcc" */ const char *cross; /* "yes" -> cross_compiling=yes; else unset */ }; /* Write a runnable script: shebang, preamble assignments, the cache * boilerplate, ONE snippet, and a dump of have_ + st_cross_ * into have.out / cross.out under temp_dir. Returns 0 on success. */ static int write_script(const char *checkname, const struct st_check_probe *p, const struct env *env) { char path[600]; FILE *f; struct st_error *err; int m; m = mkpath(path, sizeof path, "run.sh"); if (m < 0) { return -1; } f = fopen(path, "w"); if (f == NULL) { return -1; } (void)fprintf(f, "#!/bin/sh\n"); (void)fprintf(f, "CC='%s'\n", env != NULL && env->cc != NULL ? env->cc : "cc"); (void)fprintf(f, "st_cc_id='%s'\n", env != NULL && env->cc_id != NULL ? env->cc_id : "gcc"); if (env != NULL && env->cross != NULL && strcmp(env->cross, "yes") == 0) { (void)fprintf(f, "cross_compiling=yes\n"); } (void)fprintf(f, "config_cache='%s/config.cache'\n", temp_dir); (void)fprintf(f, "config_log='%s/config.log'\n", temp_dir); (void)fprintf(f, "st_tmpdir='%s'\n", temp_dir); err = st_probe_emit_cache_functions(f); if (err != NULL) { st_error_free(err); fclose(f); return -1; } err = st_probe_emit_snippet(checkname, p, f); if (err != NULL) { st_error_free(err); fclose(f); return -1; } (void)fprintf(f, "printf '%%s\\n' \"$have_%s\" > '%s/have.out'\n", checkname, temp_dir); (void)fprintf(f, "printf '%%s\\n' \"${st_cross_%s:-}\" > '%s/cross.out'\n", checkname, temp_dir); fclose(f); return 0; } /* Run "sh " via system(); return the shell's exit status or -1. */ static int run_sh(const char *path) { char cmd[700]; int rc; int n = snprintf(cmd, sizeof cmd, "sh '%s'", path); if (n < 0 || (size_t)n >= sizeof cmd) { return -1; } rc = system(cmd); if (rc == -1) { return -1; } return WEXITSTATUS(rc); } /* As run_sh, but capture the script's stderr into `err_path`. */ static int run_sh_err(const char *path, const char *err_path) { char cmd[1400]; int rc; int n = snprintf(cmd, sizeof cmd, "sh '%s' 2>'%s'", path, err_path); if (n < 0 || (size_t)n >= sizeof cmd) { return -1; } rc = system(cmd); if (rc == -1) { return -1; } return WEXITSTATUS(rc); } /* Read a tiny result file under temp_dir, strip the trailing newline. * Returns a static buffer (empty string when the file is missing). */ static const char * read_result(const char *file) { static char buf[256]; char path[600]; FILE *f; size_t n; int m; m = mkpath(path, sizeof path, file); if (m < 0) { return ""; } f = fopen(path, "r"); if (f == NULL) { return ""; } n = fread(buf, 1, sizeof buf - 1, f); fclose(f); buf[n] = '\0'; buf[strcspn(buf, "\n")] = '\0'; return buf; } /* " -n ": return the shell's exit status or -1. */ static int syntax_check(const char *shell, const char *path) { char cmd[700]; int rc; int n = snprintf(cmd, sizeof cmd, "%s -n '%s'", shell, path); if (n < 0 || (size_t)n >= sizeof cmd) { return -1; } rc = system(cmd); if (rc == -1) { return -1; } return WEXITSTATUS(rc); } /* ---- (a) snippet content ---------------------------------------------- */ static MunitResult test_header_snippet_content(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_check_probe p = { 0 }; const char *s; (void)build_from("header \"unistd.h\"", &p); munit_assert_int(p.mode, ==, ST_PROBE_COMPILE); s = emit_snippet_text("unistd_h", &p); munit_assert_not_null(s); munit_assert_not_null(strstr(s, "$CC")); munit_assert_not_null(strstr(s, "-c")); munit_assert_not_null(strstr(s, "have_unistd_h")); munit_assert_not_null(strstr(s, "have_unistd_h=yes")); munit_assert_not_null(strstr(s, "have_unistd_h=no")); munit_assert_not_null(strstr(s, "#include ")); munit_assert_not_null(strstr(s, "ac_cv_unistd_h_$st_cc_id")); munit_assert_not_null(strstr(s, "st_cache_get")); munit_assert_not_null(strstr(s, "st_cache_set")); st_check_probe_free(&p); return MUNIT_OK; } /* ---- (b) real execution: yes and no ----------------------------------- */ static MunitResult test_header_runs_yes_no(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_check_probe yes = { 0 }; struct st_check_probe no = { 0 }; char run[600]; int m; (void)build_from("header \"unistd.h\"", &yes); munit_assert_int(write_script("unistd_h", &yes, NULL), ==, 0); m = mkpath(run, sizeof run, "run.sh"); munit_assert_int(m, >, 0); munit_assert_int(run_sh(run), ==, 0); munit_assert_string_equal(read_result("have.out"), "yes"); (void)build_from("header \"nope_missing_xyz.h\"", &no); munit_assert_int(write_script("nope", &no, NULL), ==, 0); munit_assert_int(run_sh(run), ==, 0); munit_assert_string_equal(read_result("have.out"), "no"); st_check_probe_free(&yes); st_check_probe_free(&no); return MUNIT_OK; } /* ---- (c) RUN probe: cross-compile guard ------------------------------- */ static MunitResult test_run_cross_guard(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_check_probe p = { 0 }; struct env cross = { "/bin/false", "gcc", "yes" }; const char *s; char run[600]; char errp[600]; char logpath[600]; int m; int rc; (void)build_from("sizeof \"long\"", &p); munit_assert_int(p.mode, ==, ST_PROBE_RUN); /* static: the emitted snippet carries a cross_compiling guard */ s = emit_snippet_text("sz", &p); munit_assert_not_null(s); munit_assert_not_null(strstr(s, "cross_compiling")); /* dynamic: cross_compiling=yes + a failing CC -> skip, no compile */ munit_assert_int(write_script("sz", &p, &cross), ==, 0); m = mkpath(run, sizeof run, "run.sh"); munit_assert_int(m, >, 0); m = mkpath(errp, sizeof errp, "cross.err"); munit_assert_int(m, >, 0); rc = run_sh_err(run, errp); munit_assert_int(rc, ==, 0); munit_assert_string_equal(read_result("have.out"), "no"); munit_assert_string_equal(read_result("cross.out"), "yes"); /* the warning landed on stderr */ { const char *e = read_result("cross.err"); munit_assert_true(strstr(e, "cross-compiling") != NULL); } /* no compile was attempted: config.log was never created */ m = mkpath(logpath, sizeof logpath, "config.log"); munit_assert_int(m, >, 0); munit_assert_int(access(logpath, F_OK), ==, -1); /* dynamic: cross_compiling unset -> actually compiles and runs */ munit_assert_int(write_script("sz", &p, NULL), ==, 0); munit_assert_int(run_sh(run), ==, 0); munit_assert_string_equal(read_result("have.out"), "yes"); munit_assert_string_equal(read_result("cross.out"), ""); /* the run output (sizeof value) landed in config.log */ munit_assert_int(access(logpath, F_OK), ==, 0); st_check_probe_free(&p); return MUNIT_OK; } /* ---- (d) COMMAND probe: argv quoting ---------------------------------- */ static MunitResult test_command_probe_quoting(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_check_probe prog = { 0 }; struct st_check_probe pk = { 0 }; const char *s; (void)build_from("program \"pkg-config\"", &prog); s = emit_snippet_text("pgc", &prog); munit_assert_not_null(s); munit_assert_not_null(strstr(s, "command -v 'pkg-config'")); (void)build_from("pkg_config \"openssl\"", &pk); s = emit_snippet_text("ossl", &pk); munit_assert_not_null(s); munit_assert_not_null(strstr(s, "pkg-config --cflags --libs 'openssl'")); st_check_probe_free(&prog); st_check_probe_free(&pk); return MUNIT_OK; } /* ---- (e) config.cache short-circuit ----------------------------------- */ static MunitResult test_cache_short_circuit(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_check_probe p = { 0 }; struct env bad = { "/bin/false", "gcc", NULL }; char cache[600]; char run[600]; char logpath[600]; FILE *f; int m; int wr; /* pre-seed the cache with a "yes" for a header that does NOT exist, * then point CC at /bin/false: only a cache hit can yield yes. */ (void)build_from("header \"nope_missing_xyz.h\"", &p); m = mkpath(cache, sizeof cache, "config.cache"); munit_assert_int(m, >, 0); f = fopen(cache, "w"); munit_assert_not_null(f); wr = fputs("ac_cv_cached_gcc=yes\n", f); munit_assert_int(wr, !=, EOF); fclose(f); munit_assert_int(write_script("cached", &p, &bad), ==, 0); m = mkpath(run, sizeof run, "run.sh"); munit_assert_int(m, >, 0); munit_assert_int(run_sh(run), ==, 0); munit_assert_string_equal(read_result("have.out"), "yes"); /* no probe was attempted (compiler never ran) */ m = mkpath(logpath, sizeof logpath, "config.log"); munit_assert_int(m, >, 0); munit_assert_int(access(logpath, F_OK), ==, -1); /* the cached line was consumed, not overwritten with "no" */ { const char *c = read_result("config.cache"); munit_assert_not_null(strstr(c, "ac_cv_cached_gcc=yes")); } st_check_probe_free(&p); return MUNIT_OK; } static MunitResult test_cache_write(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_check_probe p = { 0 }; char run[600]; const char *c; int m; (void)build_from("header \"unistd.h\"", &p); munit_assert_int(write_script("writecc", &p, NULL), ==, 0); m = mkpath(run, sizeof run, "run.sh"); munit_assert_int(m, >, 0); munit_assert_int(run_sh(run), ==, 0); munit_assert_string_equal(read_result("have.out"), "yes"); c = read_result("config.cache"); munit_assert_not_null(strstr(c, "ac_cv_writecc_gcc=yes")); st_check_probe_free(&p); return MUNIT_OK; } /* ---- (f) syntax: sh -n / bash -n / zsh -n + banned-construct sweep ---- */ static MunitResult test_syntax_and_banned(const MunitParameter params[], void *data) { (void)params; (void)data; static const char *const banned[] = { "[[ ", "]]", "local ", "==", "<<<", "&>", "set -e", }; struct st_check_probe hdr = { 0 }; struct st_check_probe fn = { 0 }; struct st_check_probe sz = { 0 }; struct st_check_probe prog = { 0 }; char path[600]; char *bytes; FILE *f; long n; size_t i; int m; (void)build_from("header \"unistd.h\"", &hdr); (void)build_from("function \"strdup\"", &fn); (void)build_from("sizeof \"long\"", &sz); (void)build_from("program \"pkg-config\"", &prog); m = mkpath(path, sizeof path, "all.sh"); munit_assert_int(m, >, 0); f = fopen(path, "w"); munit_assert_not_null(f); (void)fprintf(f, "#!/bin/sh\n"); munit_assert_null(st_probe_emit_cache_functions(f)); munit_assert_null(st_probe_emit_snippet("hdr", &hdr, f)); munit_assert_null(st_probe_emit_snippet("fn", &fn, f)); munit_assert_null(st_probe_emit_snippet("sz", &sz, f)); munit_assert_null(st_probe_emit_snippet("prog", &prog, f)); fclose(f); munit_assert_int(syntax_check("sh", path), ==, 0); munit_assert_int(syntax_check("bash", path), ==, 0); munit_assert_int(syntax_check("zsh", path), ==, 0); /* banned-construct sweep on the real emitted bytes */ f = fopen(path, "rb"); munit_assert_not_null(f); munit_assert_int(fseek(f, 0, SEEK_END), ==, 0); n = ftell(f); munit_assert_int(n, >, 0); munit_assert_int(fseek(f, 0, SEEK_SET), ==, 0); bytes = munit_malloc((size_t)n + 1); munit_assert_size(fread(bytes, 1, (size_t)n, f), ==, (size_t)n); fclose(f); bytes[n] = '\0'; for (i = 0; i < sizeof banned / sizeof banned[0]; i++) { munit_assert_null(strstr(bytes, banned[i])); } free(bytes); st_check_probe_free(&hdr); st_check_probe_free(&fn); st_check_probe_free(&sz); st_check_probe_free(&prog); return MUNIT_OK; } /* ---- (g) injection: hostile probe target stays inert ------------------ */ static MunitResult test_injection_inert(const MunitParameter params[], void *data) { (void)params; (void)data; char marker[600]; char csrc[700]; char run[600]; int m; int rc; struct st_check_probe p; /* A C source that, if the shell ever interpreted it (unquoted), would * remove `marker`'s directory. st_sh_quote makes it a literal; the * compile just fails and the shell never executes the embedded rm. */ m = snprintf(marker, sizeof marker, "%s/victim.txt", temp_dir); munit_assert_int(m, >, 0); m = snprintf(csrc, sizeof csrc, "#include \n; rm -rf %s; \"\n", marker); munit_assert_int(m, >, 0); p = (struct st_check_probe){ .mode = ST_PROBE_COMPILE, .c_source = csrc, }; munit_assert_int(write_script("hostile", &p, NULL), ==, 0); m = mkpath(run, sizeof run, "run.sh"); munit_assert_int(m, >, 0); rc = run_sh(run); munit_assert_int(rc, ==, 0); munit_assert_string_equal(read_result("have.out"), "no"); munit_assert_int(access(marker, F_OK), ==, -1); return MUNIT_OK; } /* ---- emit_all: a feature's list of checks ----------------------------- */ static MunitResult test_emit_all(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_check_probe a = { .mode = ST_PROBE_COMPILE, .c_source = "#include \n" }; struct st_check_probe b = { .mode = ST_PROBE_COMPILE, .c_source = "#include \n" }; struct st_probe_entry entries[2]; char path[600]; FILE *f; struct st_error *err; char *bytes; long n; int m; entries[0].name = "alpha"; entries[0].probe = &a; entries[1].name = "beta"; entries[1].probe = &b; m = mkpath(path, sizeof path, "all2.sh"); munit_assert_int(m, >, 0); f = fopen(path, "w"); munit_assert_not_null(f); munit_assert_null(st_probe_emit_cache_functions(f)); err = st_probe_emit_all(entries, 2, f); munit_assert_null(err); fclose(f); f = fopen(path, "rb"); munit_assert_not_null(f); munit_assert_int(fseek(f, 0, SEEK_END), ==, 0); n = ftell(f); munit_assert_int(fseek(f, 0, SEEK_SET), ==, 0); bytes = munit_malloc((size_t)n + 1); munit_assert_size(fread(bytes, 1, (size_t)n, f), ==, (size_t)n); fclose(f); bytes[n] = '\0'; munit_assert_not_null(strstr(bytes, "have_alpha")); munit_assert_not_null(strstr(bytes, "have_beta")); munit_assert_not_null(strstr(bytes, "ac_cv_alpha_$st_cc_id")); munit_assert_not_null(strstr(bytes, "ac_cv_beta_$st_cc_id")); free(bytes); /* empty list is a clean no-op */ f = tmpfile(); munit_assert_not_null(f); err = st_probe_emit_all(NULL, 0, f); munit_assert_null(err); fclose(f); return MUNIT_OK; } /* ---- error handling ---------------------------------------------------- */ static MunitResult test_errors(const MunitParameter params[], void *data) { (void)params; (void)data; struct st_check_probe good = { .mode = ST_PROBE_COMPILE, .c_source = "#include \n" }; struct st_check_probe nocmd = { .mode = ST_PROBE_COMMAND, .command = NULL }; struct st_check_probe nosrc = { .mode = ST_PROBE_COMPILE, .c_source = NULL }; struct st_check_probe badmode = { .mode = (enum st_check_probe_mode)999, .c_source = "x" }; FILE *f = tmpfile(); struct st_error *err; munit_assert_not_null(f); err = st_probe_emit_snippet(NULL, &good, f); munit_assert_not_null(err); munit_assert_int(st_error_category_of(err), ==, ST_ERR_USAGE); st_error_free(err); err = st_probe_emit_snippet("bad name", &good, f); munit_assert_not_null(err); munit_assert_int(st_error_category_of(err), ==, ST_ERR_USAGE); st_error_free(err); err = st_probe_emit_snippet("ok", NULL, f); munit_assert_not_null(err); st_error_free(err); err = st_probe_emit_snippet("ok", &good, NULL); munit_assert_not_null(err); st_error_free(err); err = st_probe_emit_snippet("ok", &badmode, f); munit_assert_not_null(err); munit_assert_int(st_error_category_of(err), ==, ST_ERR_USAGE); st_error_free(err); err = st_probe_emit_snippet("ok", &nocmd, f); munit_assert_not_null(err); munit_assert_int(st_error_category_of(err), ==, ST_ERR_USAGE); st_error_free(err); err = st_probe_emit_snippet("ok", &nosrc, f); munit_assert_not_null(err); munit_assert_int(st_error_category_of(err), ==, ST_ERR_USAGE); st_error_free(err); err = st_probe_emit_cache_functions(NULL); munit_assert_not_null(err); st_error_free(err); /* a valid emit returns no error */ err = st_probe_emit_snippet("ok", &good, f); munit_assert_null(err); fclose(f); return MUNIT_OK; } static MunitTest tests[] = { { "/probe/header-snippet-content", test_header_snippet_content, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/probe/header-runs-yes-no", test_header_runs_yes_no, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/probe/run-cross-guard", test_run_cross_guard, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/probe/command-quoting", test_command_probe_quoting, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/probe/cache-short-circuit", test_cache_short_circuit, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/probe/cache-write", test_cache_write, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/probe/syntax-banned", test_syntax_and_banned, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/probe/injection-inert", test_injection_inert, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/probe/emit-all", test_emit_all, setup, teardown, MUNIT_TEST_OPTION_NONE, NULL }, { "/probe/errors", test_errors, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, }; static const MunitSuite suite = { "/probe", 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); }