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
+769
View File
@@ -0,0 +1,769 @@
/*
* checks.c - probe-source specs for the 8 check kinds (todo 11).
*
* Builds a `struct st_check_probe` for a schema-validated CHECK node, per
* the per-kind table documented in detect/checks.h. This module builds
* SPECS ONLY: it does not execute probes (todo 12) and does not emit shell
* text (todo 16). See the header for the struct contract and ownership.
*
* DESIGN NOTES (the decisions that are NOT obvious from the header):
*
* - FUNCTION probes carry `-fno-builtin` in extra_args. GCC treats
* ~1000 names (strdup, printf, malloc, ...) as builtins and, with
* `-Wbuiltin-declaration-mismatch` (on by default), warns on ANY
* declaration whose signature differs from the builtin's — and there
* is no single generic signature compatible with every builtin. A
* generic `extern void NAME(void); ... NAME();` is therefore wrapped
* in `-fno-builtin` so the link probe resolves the REAL library
* symbol (also preventing builtin inlining from giving a false
* positive). The function is declared `void (void)` and CALLED; the C
* ABI does not name-mangle, so linking resolves the symbol NAME
* regardless of its true signature — the probe only checks symbol
* presence and never runs the function.
*
* - LIBRARY probes link a trivial `int main(void){return 0;}` against
* `-l<name>`. The DSL carries only the library name (no function, as
* AC_CHECK_LIB requires), so "present and linkable" is the honest
* v1 meaning: the link fails if `-l<name>` cannot be found or linked.
*
* - VERSION constraints (`library`/`pkg_config` carry an optional
* `version "<op><version>"` pair) are PARSED here (`<op>` ∈
* >= <= = > <; `<version>` dotted-numeric) and the raw string is
* recorded in probe->version_constraint. v1 enforcement:
* * library: the probe is the plain `-l` link probe UNCHANGED; the
* constraint is recorded for todo 16 to enforce via pkg-config
* (or a library symbol check) when available.
* * pkg_config: the op maps to a pkg-config version flag —
* `>=` → `--atleast-version=`, `<=` → `--max-version=`,
* `=` → `--exact-version=` — added before `--cflags --libs`.
* Strict `>` and `<` have no pkg-config flag: they are recorded
* in version_constraint only (todo 16 may reject or handle them).
*
* - HEADER names are embedded as `#include <NAME>` when the name is
* "plain" (only [A-Za-z0-9_./+-]); otherwise as `#include "..."` with
* `"` and `\` C-escaped, so a hostile name (`"; rm -rf /`) stays a
* literal string and cannot break out of the directive. Any control
* byte (newline/NUL/...) in a header/function/type name is rejected.
*
* - TYPE and SIZEOF names are embedded verbatim in `sizeof(...)`; only
* control bytes are rejected (a `unsigned long`-style multi-token
* type is legitimate, and a bogus type fails the compile probe
* naturally). v1 does NOT add an AC_INCLUDES_DEFAULT-style header
* list, so `type "size_t"` tests only types visible without includes.
*
* ERRORS AND SPANS
* ----------------
* Failures are owned ST_ERR_KDL_SCHEMA errors with the span heap-allocated
* IN THE SAME BLOCK as the error (the parser.c pattern), at the offending
* token. See checks.h.
*
* Copyright (c) 2026 huntedbytheirs
* SPDX-License-Identifier: BSD-3-Clause
*/
#include "detect/checks.h"
#include "detect/check_registry.h"
#include "error.h"
#include "kdl/ast.h"
#include "kdl/value.h"
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ---- owned errors ----------------------------------------------------- */
/* Build an owned ST_ERR_KDL_SCHEMA error whose span lives in the same
* allocation as the error struct (aligned right after it). Returns NULL
* only on allocation failure. */
static struct st_error *
err_at_owned(struct st_span sp, const char *msg)
{
const size_t align = _Alignof(struct st_span);
const size_t esize =
(sizeof(struct st_error) + align - 1) & ~(align - 1);
struct st_error *e;
struct st_span *spc;
size_t mlen;
if (msg == NULL) {
msg = "";
}
e = malloc(esize + sizeof(struct st_span));
if (e == NULL) {
return NULL;
}
mlen = strlen(msg);
e->message = malloc(mlen + 1);
if (e->message == NULL) {
free(e);
return NULL;
}
memcpy(e->message, msg, mlen + 1);
e->category = ST_ERR_KDL_SCHEMA;
spc = (struct st_span *)((unsigned char *)e + esize);
*spc = sp;
e->span = spc;
return e;
}
/* Re-own a value-model (ST_ERR_KDL_PARSE) error as a schema error: copy
* the message and the span VALUE into a fresh block. The caller frees the
* original error. */
static struct st_error *
schema_err_from(const struct st_error *e)
{
struct st_span sp;
if (e->span != NULL) {
sp = *e->span;
} else {
sp = (struct st_span){ NULL, 0, 0 };
}
return err_at_owned(sp, e->message != NULL ? e->message : "");
}
/* ---- small allocators -------------------------------------------------- */
/* Duplicate a string without relying on POSIX strdup (which strict C23
* hides behind _POSIX_C_SOURCE). Returns NULL on OOM. */
static char *
xstrdup(const char *s)
{
size_t n = strlen(s);
char *p = malloc(n + 1);
if (p != NULL) {
memcpy(p, s, n + 1);
}
return p;
}
/* vsnprintf-based string builder. Returns a malloc'd NUL-terminated string
* or NULL on OOM / formatting failure. */
static char *
strbuild(const char *fmt, ...)
{
va_list ap;
va_list ap2;
int n;
char *out;
va_start(ap, fmt);
va_copy(ap2, ap);
n = vsnprintf(NULL, 0, fmt, ap);
va_end(ap);
if (n < 0) {
va_end(ap2);
return NULL;
}
out = malloc((size_t)n + 1);
if (out == NULL) {
va_end(ap2);
return NULL;
}
(void)vsnprintf(out, (size_t)n + 1, fmt, ap2);
va_end(ap2);
return out;
}
/* Free a NULL-terminated argv of heap strings (each element freed, then
* the array). NULL is a safe no-op. */
static void
argv_free(char **av)
{
size_t i;
if (av == NULL) {
return;
}
for (i = 0; av[i] != NULL; i++) {
free(av[i]);
}
free(av);
}
/* ---- argument extraction ---------------------------------------------- */
/* Interpret `tok` (with optional annotation) as the non-empty unannotated
* string a check slot requires; on success *out owns the string. `ctx`
* names the slot in messages, e.g. "check 'header' argument". */
static struct st_error *
require_string_arg(const struct st_kdl_token_ref *tok,
const struct st_kdl_token_ref *ann,
const char *ctx, char **out)
{
struct st_kdl_value v;
struct st_error *e;
char msg[192];
*out = NULL;
e = st_kdl_value_from_token_annotated(tok, ann, &v);
if (e != NULL) {
struct st_error *r = schema_err_from(e);
st_error_free(e);
return r;
}
if (v.annotation != NULL) {
snprintf(msg, sizeof msg, "%s must not carry a type annotation", ctx);
e = err_at_owned(tok->span, msg);
st_kdl_value_free(&v);
return e;
}
if (v.kind != ST_KDL_VAL_STRING) {
snprintf(msg, sizeof msg, "%s must be a non-empty string (got %s)",
ctx, st_kdl_value_kind_name(v.kind));
e = err_at_owned(tok->span, msg);
st_kdl_value_free(&v);
return e;
}
if (v.as.str[0] == '\0') {
snprintf(msg, sizeof msg, "%s must be a non-empty string", ctx);
e = err_at_owned(tok->span, msg);
st_kdl_value_free(&v);
return e;
}
*out = v.as.str;
v.as.str = NULL;
st_kdl_value_free(&v); /* frees the annotation only */
return NULL;
}
/* ---- token safety checks (C-embedding boundaries) --------------------- */
/* True when `s` contains a control byte (including NUL); these cannot
* appear in any C source token we embed verbatim. */
static bool
has_control_byte(const char *s)
{
const unsigned char *p = (const unsigned char *)s;
for (; *p != '\0'; p++) {
if (*p < 0x20u || *p == 0x7fu) {
return true;
}
}
return false;
}
/* True when `s` is a plain header name (only [A-Za-z0-9_./+-]). */
static bool
is_plain_header_name(const char *s)
{
const unsigned char *p = (const unsigned char *)s;
if (*p == '\0') {
return false;
}
for (; *p != '\0'; p++) {
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
(*p >= '0' && *p <= '9') || *p == '_' || *p == '.' ||
*p == '/' || *p == '+' || *p == '-')) {
return false;
}
}
return true;
}
/* True when `s` is a valid C identifier (letter/underscore, then letters,
* digits, underscores). */
static bool
is_c_identifier(const char *s)
{
const unsigned char *p = (const unsigned char *)s;
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
*p == '_')) {
return false;
}
for (p++; *p != '\0'; p++) {
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
(*p >= '0' && *p <= '9') || *p == '_')) {
return false;
}
}
return true;
}
/* Build the include target for a header name: `<name>` for plain names,
* `"..."` (with `"` and `\` C-escaped) otherwise. Returns a malloc'd
* string, or NULL with *err set when the name carries a control byte. */
static char *
header_ref(const char *name, struct st_error **err)
{
if (err != NULL) {
*err = NULL;
}
if (has_control_byte(name)) {
char msg[192];
snprintf(msg, sizeof msg,
"check 'header': header name contains a control character");
if (err != NULL) {
*err = st_error_kdl_schema(msg);
}
return NULL;
}
if (is_plain_header_name(name)) {
return strbuild("<%s>", name);
}
{
/* escape `\` and `"` for the quoted include form */
size_t i;
size_t cap = strlen(name) * 2 + 3; /* 2x for worst-case escapes + "" NUL */
char *out = malloc(cap);
size_t n = 0;
if (out == NULL) {
return NULL;
}
out[n++] = '"';
for (i = 0; name[i] != '\0'; i++) {
if (name[i] == '\\' || name[i] == '"') {
out[n++] = '\\';
}
out[n++] = name[i];
}
out[n++] = '"';
out[n] = '\0';
return out;
}
}
/* ---- version constraint parsing --------------------------------------- */
/* Parse "<op><version>". On success returns 0 and sets *ver (a pointer
* into `s`, NUL-terminated because `s` is). The version part must be
* non-empty dotted-numeric (digits and '.', at least one digit). Returns
* -1 otherwise. */
static int
parse_constraint(const char *s, const char **ver)
{
size_t ol = 0;
const char *v;
size_t i;
size_t ndigits = 0;
if (s[0] == '>' && s[1] == '=') {
ol = 2;
} else if (s[0] == '<' && s[1] == '=') {
ol = 2;
} else if (s[0] == '>' || s[0] == '<' || s[0] == '=') {
ol = 1;
} else {
return -1;
}
v = s + ol;
if (v[0] == '\0') {
return -1;
}
for (i = 0; v[i] != '\0'; i++) {
if (v[i] >= '0' && v[i] <= '9') {
ndigits++;
} else if (v[i] != '.') {
return -1;
}
}
if (ndigits == 0) {
return -1;
}
*ver = v;
return 0;
}
/* ---- per-kind source builders ----------------------------------------- */
/* The trivial main shared by library and compiler_flag probes. */
static char *
trivial_main_source(void)
{
return xstrdup(
"int main(void)\n"
"{\n"
" return 0;\n"
"}\n");
}
/* ---- public API ------------------------------------------------------- */
struct st_error *
st_check_probe_build(enum st_check_kind kind, const struct st_kdl_node *node,
struct st_check_probe *out)
{
struct st_check_probe tmp = { ST_PROBE_PREPROCESS, NULL, NULL, NULL,
NULL };
const struct st_check_shape *shape;
struct st_error *err = NULL;
char *arg = NULL;
char *constraint = NULL; /* raw "<op><version>" for library/pkg_config */
const char *con_ver = NULL; /* parsed version pointer into `constraint` */
char ctx[96];
if (out != NULL) {
*out = (struct st_check_probe){ ST_PROBE_PREPROCESS, NULL, NULL, NULL,
NULL };
}
if ((size_t)kind >= ST_CHECK_KIND_COUNT) {
return st_error_kdl_schema(
"st_check_probe_build: unknown check kind");
}
if (node == NULL) {
return st_error_kdl_schema("check node is NULL");
}
shape = &st_check_shapes[kind];
/* the target argument */
if (node->args == NULL) {
char msg[192];
snprintf(msg, sizeof msg,
"check '%s' requires an argument (the %s)", shape->name,
shape->arg_meaning);
err = err_at_owned(node->name.span, msg);
goto done;
}
snprintf(ctx, sizeof ctx, "check '%s' argument", shape->name);
err = require_string_arg(&node->args->value, node->args->annotation, ctx,
&arg);
if (err != NULL) {
goto done;
}
/* the optional `version` keyword + constraint pair (library/pkg_config) */
if (shape->optional_args == 2 && node->args->next != NULL) {
const struct st_kdl_arg *vk = node->args->next;
char msg[192];
if (vk->annotation != NULL ||
!(vk->value.kind == ST_TOK_IDENT && vk->value.len == 7 &&
memcmp(vk->value.text, "version", 7) == 0)) {
snprintf(msg, sizeof msg,
"check '%s': expected the keyword 'version' as the second "
"argument", shape->name);
err = err_at_owned(vk->value.span, msg);
goto done;
}
if (vk->next == NULL) {
snprintf(msg, sizeof msg,
"check '%s': 'version' requires the version constraint "
"argument", shape->name);
err = err_at_owned(vk->value.span, msg);
goto done;
}
snprintf(ctx, sizeof ctx, "check '%s' version", shape->name);
err = require_string_arg(&vk->next->value, vk->next->annotation, ctx,
&constraint);
if (err != NULL) {
goto done;
}
}
/* Validate "<op><version>" for BOTH kinds; `library` only records it
* (enforcement is todo 16), so this runs even where it is not applied. */
if (constraint != NULL) {
char msg[192];
if (parse_constraint(constraint, &con_ver) != 0) {
snprintf(msg, sizeof msg,
"check '%s': malformed version constraint '%s' "
"(expected '<op><version>', e.g. '>=1.0')", shape->name,
constraint);
err = err_at_owned(node->args->next->next->value.span, msg);
goto done;
}
}
switch (kind) {
case ST_CHECK_HEADER: {
char *ref = header_ref(arg, &err);
if (err != NULL) {
goto done;
}
if (ref == NULL) {
err = st_error_internal("out of memory building header probe");
goto done;
}
tmp.mode = ST_PROBE_COMPILE;
tmp.c_source = strbuild("#include %s\n", ref);
free(ref);
if (tmp.c_source == NULL) {
err = st_error_internal("out of memory building header probe");
goto done;
}
break;
}
case ST_CHECK_FUNCTION: {
char msg[192];
if (!is_c_identifier(arg)) {
snprintf(msg, sizeof msg,
"check 'function': function name '%s' is not a valid C "
"identifier", arg);
err = err_at_owned(node->args->value.span, msg);
goto done;
}
tmp.mode = ST_PROBE_LINK;
tmp.c_source = strbuild(
"extern void %s(void);\n"
"int main(void)\n"
"{\n"
" %s();\n"
" return 0;\n"
"}\n",
arg, arg);
if (tmp.c_source == NULL) {
err = st_error_internal("out of memory building function probe");
goto done;
}
tmp.extra_args = calloc(2, sizeof(char *));
if (tmp.extra_args == NULL) {
err = st_error_internal("out of memory building function probe");
goto done;
}
tmp.extra_args[0] = xstrdup("-fno-builtin");
if (tmp.extra_args[0] == NULL) {
err = st_error_internal("out of memory building function probe");
goto done;
}
break;
}
case ST_CHECK_LIBRARY: {
tmp.mode = ST_PROBE_LINK;
tmp.c_source = trivial_main_source();
if (tmp.c_source == NULL) {
err = st_error_internal("out of memory building library probe");
goto done;
}
tmp.extra_args = calloc(2, sizeof(char *));
if (tmp.extra_args == NULL) {
err = st_error_internal("out of memory building library probe");
goto done;
}
tmp.extra_args[0] = strbuild("-l%s", arg);
if (tmp.extra_args[0] == NULL) {
err = st_error_internal("out of memory building library probe");
goto done;
}
if (constraint != NULL) {
tmp.version_constraint = xstrdup(constraint);
if (tmp.version_constraint == NULL) {
err = st_error_internal("out of memory building library "
"probe");
goto done;
}
}
break;
}
case ST_CHECK_TYPE: {
char msg[192];
if (has_control_byte(arg)) {
snprintf(msg, sizeof msg,
"check 'type': type name contains a control character");
err = err_at_owned(node->args->value.span, msg);
goto done;
}
tmp.mode = ST_PROBE_COMPILE;
tmp.c_source = strbuild(
"_Static_assert(sizeof(%s) != 0, \"type must be a complete "
"object type\");\n",
arg);
if (tmp.c_source == NULL) {
err = st_error_internal("out of memory building type probe");
goto done;
}
break;
}
case ST_CHECK_SIZEOF: {
char msg[192];
if (has_control_byte(arg)) {
snprintf(msg, sizeof msg,
"check 'sizeof': type name contains a control character");
err = err_at_owned(node->args->value.span, msg);
goto done;
}
tmp.mode = ST_PROBE_RUN;
tmp.c_source = strbuild(
"#include <stdio.h>\n"
"_Static_assert(sizeof(%s) != 0, \"type must be a complete "
"object type\");\n"
"int main(void)\n"
"{\n"
" printf(\"%%zu\\n\", sizeof(%s));\n"
" return 0;\n"
"}\n",
arg, arg);
if (tmp.c_source == NULL) {
err = st_error_internal("out of memory building sizeof probe");
goto done;
}
break;
}
case ST_CHECK_PROGRAM: {
tmp.mode = ST_PROBE_COMMAND;
tmp.command = calloc(4, sizeof(char *));
if (tmp.command == NULL) {
err = st_error_internal("out of memory building program probe");
goto done;
}
tmp.command[0] = xstrdup("command");
tmp.command[1] = xstrdup("-v");
tmp.command[2] = xstrdup(arg);
if (tmp.command[0] == NULL || tmp.command[1] == NULL ||
tmp.command[2] == NULL) {
err = st_error_internal("out of memory building program probe");
goto done;
}
break;
}
case ST_CHECK_COMPILER_FLAG: {
tmp.mode = ST_PROBE_COMPILE;
tmp.c_source = trivial_main_source();
if (tmp.c_source == NULL) {
err = st_error_internal("out of memory building compiler_flag "
"probe");
goto done;
}
tmp.extra_args = calloc(2, sizeof(char *));
if (tmp.extra_args == NULL) {
err = st_error_internal("out of memory building compiler_flag "
"probe");
goto done;
}
tmp.extra_args[0] = xstrdup(arg);
if (tmp.extra_args[0] == NULL) {
err = st_error_internal("out of memory building compiler_flag "
"probe");
goto done;
}
break;
}
case ST_CHECK_PKG_CONFIG: {
const char *ver_flag = NULL;
if (constraint != NULL) {
if (constraint[0] == '>' && constraint[1] == '=') {
ver_flag = "--atleast-version=";
} else if (constraint[0] == '<' && constraint[1] == '=') {
ver_flag = "--max-version=";
} else if (constraint[0] == '=') {
ver_flag = "--exact-version=";
}
/* strict '>' and '<' have no pkg-config flag: recorded only */
tmp.version_constraint = xstrdup(constraint);
if (tmp.version_constraint == NULL) {
err = st_error_internal("out of memory building pkg_config "
"probe");
goto done;
}
}
tmp.mode = ST_PROBE_COMMAND;
if (ver_flag != NULL) {
char *flag = strbuild("%s%s", ver_flag, con_ver);
if (flag == NULL) {
err = st_error_internal("out of memory building pkg_config "
"probe");
goto done;
}
tmp.command = calloc(6, sizeof(char *));
if (tmp.command == NULL) {
free(flag);
err = st_error_internal("out of memory building pkg_config "
"probe");
goto done;
}
tmp.command[0] = xstrdup("pkg-config");
tmp.command[1] = flag;
tmp.command[2] = xstrdup("--cflags");
tmp.command[3] = xstrdup("--libs");
tmp.command[4] = xstrdup(arg);
if (tmp.command[0] == NULL || tmp.command[2] == NULL ||
tmp.command[3] == NULL || tmp.command[4] == NULL) {
err = st_error_internal("out of memory building pkg_config "
"probe");
goto done;
}
} else {
tmp.command = calloc(5, sizeof(char *));
if (tmp.command == NULL) {
err = st_error_internal("out of memory building pkg_config "
"probe");
goto done;
}
tmp.command[0] = xstrdup("pkg-config");
tmp.command[1] = xstrdup("--cflags");
tmp.command[2] = xstrdup("--libs");
tmp.command[3] = xstrdup(arg);
if (tmp.command[0] == NULL || tmp.command[1] == NULL ||
tmp.command[2] == NULL || tmp.command[3] == NULL) {
err = st_error_internal("out of memory building pkg_config "
"probe");
goto done;
}
}
break;
}
default:
err = st_error_kdl_schema("st_check_probe_build: unknown check kind");
goto done;
}
done:
free(arg);
free(constraint);
if (err != NULL) {
st_check_probe_free(&tmp);
return err;
}
if (out != NULL) {
*out = tmp;
} else {
st_check_probe_free(&tmp);
}
return NULL;
}
void
st_check_probe_free(struct st_check_probe *p)
{
if (p == NULL) {
return;
}
free(p->c_source);
free(p->version_constraint);
argv_free(p->command);
argv_free(p->extra_args);
p->mode = ST_PROBE_PREPROCESS;
p->c_source = NULL;
p->version_constraint = NULL;
p->command = NULL;
p->extra_args = NULL;
}
const char *
st_check_probe_mode_name(enum st_check_probe_mode mode)
{
switch (mode) {
case ST_PROBE_PREPROCESS:
return "preprocess";
case ST_PROBE_COMPILE:
return "compile";
case ST_PROBE_LINK:
return "link";
case ST_PROBE_RUN:
return "run";
case ST_PROBE_COMMAND:
return "command";
default:
return "?";
}
}
+131
View File
@@ -0,0 +1,131 @@
/*
* checks.h - probe-source specs for the 8 check kinds (todo 11).
*
* Given a feature's CHECK node (already schema-validated by src/kdl/schema.c),
* this module builds the exact PROBE SPEC that todo 12 (probe code generator)
* turns into configure-time compile/link/run/command invocations and todo 16
* (configure assembly) substitutes the results of. It builds SPECS ONLY: it
* never executes a probe, never touches the filesystem beyond heap
* allocation, and never emits shell text.
*
* THE PROBE SPEC (the contract todo 12/16 consume)
* ------------------------------------------------
*
* enum st_check_probe_mode {
* ST_PROBE_PREPROCESS, // -E: header existence via the preprocessor
* ST_PROBE_COMPILE, // compile a C translation unit (-c)
* ST_PROBE_LINK, // compile + link a C program
* ST_PROBE_RUN, // compile + link + RUN, capture stdout
* ST_PROBE_COMMAND, // run an external command via execvp (argv)
* };
*
* struct st_check_probe {
* enum st_check_probe_mode mode;
* char *c_source; // C snippet; NULL for COMMAND probes
* char **command; // argv (NULL-terminated); NULL unless
* // mode == ST_PROBE_COMMAND
* char **extra_args; // argv (NULL-terminated) of extra
* // compile/link flags (e.g. "-lpthread",
* // "-fsanitize=address",
* // "-fno-builtin"); NULL when empty
* char *version_constraint; // raw "<op><version>" string for
* // library/pkg_config; NULL when absent
* };
*
* - `command` and `extra_args` are exec-style argv arrays (char **, first
* element is argv[0] for `command`, a flag for `extra_args`; both
* NULL-terminated). The program/package name a COMMAND probe targets is
* carried as a SINGLE argv element, never concatenated into a shell
* string — todo 12/16 shell-quote it at emission (st_sh_quote) so
* injection is structurally impossible here.
* - `c_source` is a complete, self-contained C23 translation unit (or a
* standalone declaration for compile-only probes) that compiles clean
* under `-std=c23 -Wall -Wextra -Wpedantic`.
*
* PER-KIND SPEC TABLE
* -------------------
* header "pthread.h" COMPILE `#include <pthread.h>` (angle form for
* plain names; `"..."` escaped form for
* names with spaces/quotes/metachars)
* function "strdup" LINK `extern void strdup(void); ...`
* + a call; extra_args = {"-fno-builtin"}
* library "pthread" LINK trivial `int main(void){return 0;}`
* + extra_args = {"-lpthread"}
* type "size_t" COMPILE `_Static_assert(sizeof(size_t) != 0,...)`
* sizeof "long" RUN `_Static_assert(...)` sentinel + a
* printf of `sizeof(long)` (run path)
* program "pkg-config" COMMAND argv {command, -v, pkg-config}
* compiler_flag "-fsani..." COMPILE trivial main + extra_args = {flag}
* pkg_config "openssl" COMMAND argv {pkg-config, --cflags, --libs, pkg}
*
* OWNERSHIP
* ---------
* st_check_probe_build() writes a FULLY HEAP-OWNED probe into a caller
* provided struct; st_check_probe_free() releases it (NULL is a safe no-op).
* On error `out` is left zeroed and an owned st_error is returned.
*
* ERRORS
* ------
* Failures return an owned st_error of category ST_ERR_KDL_SCHEMA whose span
* is heap-allocated IN THE SAME BLOCK as the error (the parser.c pattern), at
* the offending token: a NULL node, a missing/non-string target argument, a
* malformed version constraint, a non-identifier function name, or a control
* character in an embedded C token. An out-of-range kind is the one span-less
* case (defensive; no source position exists). On allocation failure the
* error is a plain ST_ERR_INTERNAL with no span (and, as throughout this
* codebase, may itself be NULL under catastrophic OOM).
*
* Copyright (c) 2026 huntedbytheirs
* SPDX-License-Identifier: BSD-3-Clause
*/
#ifndef ST_DETECT_CHECKS_H
#define ST_DETECT_CHECKS_H
#include <stddef.h>
#include "detect/check_registry.h" /* enum st_check_kind */
struct st_error;
struct st_kdl_node;
/* The probe MODE: what todo 12 must do with the snippet to test the check.
* PREPROCESS is reserved for a future header fast-path; none of the 8 v1
* kinds emit it (header uses COMPILE). */
enum st_check_probe_mode {
ST_PROBE_PREPROCESS = 0, /* -E the snippet (unused by v1 kinds) */
ST_PROBE_COMPILE, /* compile the snippet (-c) */
ST_PROBE_LINK, /* compile + link the snippet */
ST_PROBE_RUN, /* compile + link + run, capture stdout */
ST_PROBE_COMMAND, /* exec the `command` argv */
};
/* A built probe spec (see the header comment for the per-kind table). */
struct st_check_probe {
enum st_check_probe_mode mode;
char *c_source; /* heap C snippet; NULL for COMMAND probes */
char **command; /* heap argv (NULL-terminated); NULL unless
mode == ST_PROBE_COMMAND */
char **extra_args; /* heap argv (NULL-terminated) of extra
compile/link flags; NULL when empty */
char *version_constraint; /* heap raw "<op><version>"; NULL when absent */
};
/* Build the probe spec for `kind` from a schema-validated CHECK node. The
* node's FIRST positional argument is the target; for library/pkg_config the
* optional `version` keyword + constraint pair is read too. On success
* returns NULL and fills `*out` (a fully heap-owned probe). On failure
* returns an owned error (see ERRORS) and leaves `*out` zeroed. */
struct st_error *st_check_probe_build(enum st_check_kind kind,
const struct st_kdl_node *node,
struct st_check_probe *out);
/* Release a probe spec produced by st_check_probe_build. NULL is a safe
* no-op; the probe is zeroed after freeing. */
void st_check_probe_free(struct st_check_probe *p);
/* Stable display name for a probe mode, e.g. "compile". Out-of-range modes
* yield "?" rather than indexing out of range. */
const char *st_check_probe_mode_name(enum st_check_probe_mode mode);
#endif /* ST_DETECT_CHECKS_H */
+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);
}