selftests/bpf: __not_msg() tag for test_loader framework

This patch adds tags __not_msg(<msg>) and __not_msg_unpriv(<msg>).
Test fails if <msg> is found in verifier log.

If __msg_not() is situated between __msg() tags framework matches
__msg() tags first, and then checks that <msg> is not present in a
portion of a log between bracketing __msg() tags.
__msg_not() tags bracketed by a same __msg() group are effectively
unordered.

The idea is borrowed from LLVM's CheckFile with its CHECK-NOT syntax.

Signed-off-by: Eduard Zingerman <eddyz87@gmail.com>
Link: https://lore.kernel.org/r/20250918-callchain-sensitive-liveness-v3-11-c3cd27bacc60@gmail.com
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
This commit is contained in:
Eduard Zingerman
2025-09-18 19:18:44 -07:00
committed by Alexei Starovoitov
parent 79f047c7d9
commit 34c513be3d
4 changed files with 301 additions and 49 deletions

View File

@@ -54,3 +54,128 @@ void test_prog_tests_framework(void)
return;
clear_test_state(state);
}
static void dummy_emit(const char *buf, bool force) {}
void test_prog_tests_framework_expected_msgs(void)
{
struct expected_msgs msgs;
int i, j, error_cnt;
const struct {
const char *name;
const char *log;
const char *expected;
struct expect_msg *pats;
} cases[] = {
{
.name = "simple-ok",
.log = "aaabbbccc",
.pats = (struct expect_msg[]) {
{ .substr = "aaa" },
{ .substr = "ccc" },
{}
}
},
{
.name = "simple-fail",
.log = "aaabbbddd",
.expected = "MATCHED SUBSTR: 'aaa'\n"
"EXPECTED SUBSTR: 'ccc'\n",
.pats = (struct expect_msg[]) {
{ .substr = "aaa" },
{ .substr = "ccc" },
{}
}
},
{
.name = "negative-ok-mid",
.log = "aaabbbccc",
.pats = (struct expect_msg[]) {
{ .substr = "aaa" },
{ .substr = "foo", .negative = true },
{ .substr = "bar", .negative = true },
{ .substr = "ccc" },
{}
}
},
{
.name = "negative-ok-tail",
.log = "aaabbbccc",
.pats = (struct expect_msg[]) {
{ .substr = "aaa" },
{ .substr = "foo", .negative = true },
{}
}
},
{
.name = "negative-ok-head",
.log = "aaabbbccc",
.pats = (struct expect_msg[]) {
{ .substr = "foo", .negative = true },
{ .substr = "ccc" },
{}
}
},
{
.name = "negative-fail-head",
.log = "aaabbbccc",
.expected = "UNEXPECTED SUBSTR: 'aaa'\n",
.pats = (struct expect_msg[]) {
{ .substr = "aaa", .negative = true },
{ .substr = "bbb" },
{}
}
},
{
.name = "negative-fail-tail",
.log = "aaabbbccc",
.expected = "UNEXPECTED SUBSTR: 'ccc'\n",
.pats = (struct expect_msg[]) {
{ .substr = "bbb" },
{ .substr = "ccc", .negative = true },
{}
}
},
{
.name = "negative-fail-mid-1",
.log = "aaabbbccc",
.expected = "UNEXPECTED SUBSTR: 'bbb'\n",
.pats = (struct expect_msg[]) {
{ .substr = "aaa" },
{ .substr = "bbb", .negative = true },
{ .substr = "ccc" },
{}
}
},
{
.name = "negative-fail-mid-2",
.log = "aaabbb222ccc",
.expected = "UNEXPECTED SUBSTR: '222'\n",
.pats = (struct expect_msg[]) {
{ .substr = "aaa" },
{ .substr = "222", .negative = true },
{ .substr = "bbb", .negative = true },
{ .substr = "ccc" },
{}
}
}
};
for (i = 0; i < ARRAY_SIZE(cases); i++) {
if (test__start_subtest(cases[i].name)) {
error_cnt = env.subtest_state->error_cnt;
msgs.patterns = cases[i].pats;
msgs.cnt = 0;
for (j = 0; cases[i].pats[j].substr; j++)
msgs.cnt++;
validate_msgs(cases[i].log, &msgs, dummy_emit);
fflush(stderr);
env.subtest_state->error_cnt = error_cnt;
if (cases[i].expected)
ASSERT_HAS_SUBSTR(env.subtest_state->log_buf, cases[i].expected, "expected output");
else
ASSERT_STREQ(env.subtest_state->log_buf, "", "expected no output");
test__end_subtest();
}
}
}

View File

@@ -33,7 +33,14 @@
* e.g. "foo{{[0-9]+}}" matches strings like "foo007".
* Extended POSIX regular expression syntax is allowed
* inside the brackets.
* __not_msg Message not expected to be found in verifier log.
* If __msg_not is situated between __msg tags
* framework matches __msg tags first, and then
* checks that __msg_not is not present in a portion of
* a log between bracketing __msg tags.
* Same regex syntax as for __msg is supported.
* __msg_unpriv Same as __msg but for unprivileged mode.
* __not_msg_unpriv Same as __not_msg but for unprivileged mode.
*
* __stderr Message expected to be found in bpf stderr stream. The
* same regex rules apply like __msg.
@@ -121,12 +128,14 @@
* __caps_unpriv Specify the capabilities that should be set when running the test.
*/
#define __msg(msg) __attribute__((btf_decl_tag("comment:test_expect_msg=" XSTR(__COUNTER__) "=" msg)))
#define __not_msg(msg) __attribute__((btf_decl_tag("comment:test_expect_not_msg=" XSTR(__COUNTER__) "=" msg)))
#define __xlated(msg) __attribute__((btf_decl_tag("comment:test_expect_xlated=" XSTR(__COUNTER__) "=" msg)))
#define __jited(msg) __attribute__((btf_decl_tag("comment:test_jited=" XSTR(__COUNTER__) "=" msg)))
#define __failure __attribute__((btf_decl_tag("comment:test_expect_failure")))
#define __success __attribute__((btf_decl_tag("comment:test_expect_success")))
#define __description(desc) __attribute__((btf_decl_tag("comment:test_description=" desc)))
#define __msg_unpriv(msg) __attribute__((btf_decl_tag("comment:test_expect_msg_unpriv=" XSTR(__COUNTER__) "=" msg)))
#define __not_msg_unpriv(msg) __attribute__((btf_decl_tag("comment:test_expect_not_msg_unpriv=" XSTR(__COUNTER__) "=" msg)))
#define __xlated_unpriv(msg) __attribute__((btf_decl_tag("comment:test_expect_xlated_unpriv=" XSTR(__COUNTER__) "=" msg)))
#define __jited_unpriv(msg) __attribute__((btf_decl_tag("comment:test_jited=" XSTR(__COUNTER__) "=" msg)))
#define __failure_unpriv __attribute__((btf_decl_tag("comment:test_expect_failure_unpriv")))

View File

@@ -2,7 +2,6 @@
/* Copyright (c) 2022 Meta Platforms, Inc. and affiliates. */
#include <linux/capability.h>
#include <stdlib.h>
#include <regex.h>
#include <test_progs.h>
#include <bpf/btf.h>
@@ -20,10 +19,12 @@
#define TEST_TAG_EXPECT_FAILURE "comment:test_expect_failure"
#define TEST_TAG_EXPECT_SUCCESS "comment:test_expect_success"
#define TEST_TAG_EXPECT_MSG_PFX "comment:test_expect_msg="
#define TEST_TAG_EXPECT_NOT_MSG_PFX "comment:test_expect_not_msg="
#define TEST_TAG_EXPECT_XLATED_PFX "comment:test_expect_xlated="
#define TEST_TAG_EXPECT_FAILURE_UNPRIV "comment:test_expect_failure_unpriv"
#define TEST_TAG_EXPECT_SUCCESS_UNPRIV "comment:test_expect_success_unpriv"
#define TEST_TAG_EXPECT_MSG_PFX_UNPRIV "comment:test_expect_msg_unpriv="
#define TEST_TAG_EXPECT_NOT_MSG_PFX_UNPRIV "comment:test_expect_not_msg_unpriv="
#define TEST_TAG_EXPECT_XLATED_PFX_UNPRIV "comment:test_expect_xlated_unpriv="
#define TEST_TAG_LOG_LEVEL_PFX "comment:test_log_level="
#define TEST_TAG_PROG_FLAGS_PFX "comment:test_prog_flags="
@@ -65,18 +66,6 @@ enum load_mode {
NO_JITED = 1 << 1,
};
struct expect_msg {
const char *substr; /* substring match */
regex_t regex;
bool is_regex;
bool on_next_line;
};
struct expected_msgs {
struct expect_msg *patterns;
size_t cnt;
};
struct test_subspec {
char *name;
bool expect_failure;
@@ -216,7 +205,8 @@ static int compile_regex(const char *pattern, regex_t *regex)
return 0;
}
static int __push_msg(const char *pattern, bool on_next_line, struct expected_msgs *msgs)
static int __push_msg(const char *pattern, bool on_next_line, bool negative,
struct expected_msgs *msgs)
{
struct expect_msg *msg;
void *tmp;
@@ -232,6 +222,7 @@ static int __push_msg(const char *pattern, bool on_next_line, struct expected_ms
msg = &msgs->patterns[msgs->cnt];
msg->on_next_line = on_next_line;
msg->substr = pattern;
msg->negative = negative;
msg->is_regex = false;
if (strstr(pattern, "{{")) {
err = compile_regex(pattern, &msg->regex);
@@ -250,16 +241,16 @@ static int clone_msgs(struct expected_msgs *from, struct expected_msgs *to)
for (i = 0; i < from->cnt; i++) {
msg = &from->patterns[i];
err = __push_msg(msg->substr, msg->on_next_line, to);
err = __push_msg(msg->substr, msg->on_next_line, msg->negative, to);
if (err)
return err;
}
return 0;
}
static int push_msg(const char *substr, struct expected_msgs *msgs)
static int push_msg(const char *substr, bool negative, struct expected_msgs *msgs)
{
return __push_msg(substr, false, msgs);
return __push_msg(substr, false, negative, msgs);
}
static int push_disasm_msg(const char *regex_str, bool *on_next_line, struct expected_msgs *msgs)
@@ -270,7 +261,7 @@ static int push_disasm_msg(const char *regex_str, bool *on_next_line, struct exp
*on_next_line = false;
return 0;
}
err = __push_msg(regex_str, *on_next_line, msgs);
err = __push_msg(regex_str, *on_next_line, false, msgs);
if (err)
return err;
*on_next_line = true;
@@ -482,12 +473,22 @@ static int parse_test_spec(struct test_loader *tester,
spec->auxiliary = true;
spec->mode_mask |= UNPRIV;
} else if ((msg = skip_dynamic_pfx(s, TEST_TAG_EXPECT_MSG_PFX))) {
err = push_msg(msg, &spec->priv.expect_msgs);
err = push_msg(msg, false, &spec->priv.expect_msgs);
if (err)
goto cleanup;
spec->mode_mask |= PRIV;
} else if ((msg = skip_dynamic_pfx(s, TEST_TAG_EXPECT_NOT_MSG_PFX))) {
err = push_msg(msg, true, &spec->priv.expect_msgs);
if (err)
goto cleanup;
spec->mode_mask |= PRIV;
} else if ((msg = skip_dynamic_pfx(s, TEST_TAG_EXPECT_MSG_PFX_UNPRIV))) {
err = push_msg(msg, &spec->unpriv.expect_msgs);
err = push_msg(msg, false, &spec->unpriv.expect_msgs);
if (err)
goto cleanup;
spec->mode_mask |= UNPRIV;
} else if ((msg = skip_dynamic_pfx(s, TEST_TAG_EXPECT_NOT_MSG_PFX_UNPRIV))) {
err = push_msg(msg, true, &spec->unpriv.expect_msgs);
if (err)
goto cleanup;
spec->mode_mask |= UNPRIV;
@@ -764,44 +765,141 @@ static void emit_stdout(const char *bpf_stdout, bool force)
fprintf(stdout, "STDOUT:\n=============\n%s=============\n", bpf_stdout);
}
static void validate_msgs(char *log_buf, struct expected_msgs *msgs,
void (*emit_fn)(const char *buf, bool force))
static const char *match_msg(struct expect_msg *msg, const char **log)
{
const char *log = log_buf, *prev_match;
const char *match = NULL;
regmatch_t reg_match[1];
int prev_match_line;
int match_line;
int i, j, err;
int err;
if (!msg->is_regex) {
match = strstr(*log, msg->substr);
if (match)
*log = match + strlen(msg->substr);
} else {
err = regexec(&msg->regex, *log, 1, reg_match, 0);
if (err == 0) {
match = *log + reg_match[0].rm_so;
*log += reg_match[0].rm_eo;
}
}
return match;
}
static int count_lines(const char *start, const char *end)
{
const char *tmp;
int n = 0;
for (tmp = start; tmp < end; ++tmp)
if (*tmp == '\n')
n++;
return n;
}
struct match {
const char *start;
const char *end;
int line;
};
/*
* Positive messages are matched sequentially, each next message
* is looked for starting from the end of a previous matched one.
*/
static void match_positive_msgs(const char *log, struct expected_msgs *msgs, struct match *matches)
{
const char *prev_match;
int i, line;
prev_match_line = -1;
match_line = 0;
prev_match = log;
line = 0;
for (i = 0; i < msgs->cnt; i++) {
struct expect_msg *msg = &msgs->patterns[i];
const char *match = NULL, *pat_status;
bool wrong_line = false;
const char *match = NULL;
if (!msg->is_regex) {
match = strstr(log, msg->substr);
if (match)
log = match + strlen(msg->substr);
} else {
err = regexec(&msg->regex, log, 1, reg_match, 0);
if (err == 0) {
match = log + reg_match[0].rm_so;
log += reg_match[0].rm_eo;
if (msg->negative)
continue;
match = match_msg(msg, &log);
if (match) {
line += count_lines(prev_match, match);
matches[i].start = match;
matches[i].end = log;
matches[i].line = line;
prev_match = match;
}
}
}
/*
* Each negative messages N located between positive messages P1 and P2
* is matched in the span P1.end .. P2.start. Consequently, negative messages
* are unordered within the span.
*/
static void match_negative_msgs(const char *log, struct expected_msgs *msgs, struct match *matches)
{
const char *start = log, *end, *next, *match;
const char *log_end = log + strlen(log);
int i, j, next_positive;
for (i = 0; i < msgs->cnt; i++) {
struct expect_msg *msg = &msgs->patterns[i];
/* positive message bumps span start */
if (!msg->negative) {
start = matches[i].end ?: start;
continue;
}
/* count stride of negative patterns and adjust span end */
end = log_end;
for (next_positive = i + 1; next_positive < msgs->cnt; next_positive++) {
if (!msgs->patterns[next_positive].negative) {
end = matches[next_positive].start;
break;
}
}
if (match) {
for (; prev_match < match; ++prev_match)
if (*prev_match == '\n')
++match_line;
wrong_line = msg->on_next_line && prev_match_line >= 0 &&
prev_match_line + 1 != match_line;
/* try matching negative messages within identified span */
for (j = i; j < next_positive; j++) {
next = start;
match = match_msg(msg, &next);
if (match && next <= end) {
matches[j].start = match;
matches[j].end = next;
}
}
if (!match || wrong_line) {
/* -1 to account for i++ */
i = next_positive - 1;
}
}
void validate_msgs(const char *log_buf, struct expected_msgs *msgs,
void (*emit_fn)(const char *buf, bool force))
{
struct match matches[msgs->cnt];
struct match *prev_match = NULL;
int i, j;
memset(matches, 0, sizeof(*matches) * msgs->cnt);
match_positive_msgs(log_buf, msgs, matches);
match_negative_msgs(log_buf, msgs, matches);
for (i = 0; i < msgs->cnt; i++) {
struct expect_msg *msg = &msgs->patterns[i];
struct match *match = &matches[i];
const char *pat_status;
bool unexpected;
bool wrong_line;
bool no_match;
no_match = !msg->negative && !match->start;
wrong_line = !msg->negative &&
msg->on_next_line &&
prev_match && prev_match->line + 1 != match->line;
unexpected = msg->negative && match->start;
if (no_match || wrong_line || unexpected) {
PRINT_FAIL("expect_msg\n");
if (env.verbosity == VERBOSE_NONE)
emit_fn(log_buf, true /*force*/);
@@ -811,8 +909,10 @@ static void validate_msgs(char *log_buf, struct expected_msgs *msgs,
pat_status = "MATCHED ";
else if (wrong_line)
pat_status = "WRONG LINE";
else
else if (no_match)
pat_status = "EXPECTED ";
else
pat_status = "UNEXPECTED";
msg = &msgs->patterns[j];
fprintf(stderr, "%s %s: '%s'\n",
pat_status,
@@ -822,12 +922,13 @@ static void validate_msgs(char *log_buf, struct expected_msgs *msgs,
if (wrong_line) {
fprintf(stderr,
"expecting match at line %d, actual match is at line %d\n",
prev_match_line + 1, match_line);
prev_match->line + 1, match->line);
}
break;
}
prev_match_line = match_line;
if (!msg->negative)
prev_match = match;
}
}

View File

@@ -7,6 +7,7 @@
#include <errno.h>
#include <string.h>
#include <assert.h>
#include <regex.h>
#include <stdlib.h>
#include <stdarg.h>
#include <time.h>
@@ -546,4 +547,20 @@ extern void test_loader_fini(struct test_loader *tester);
test_loader_fini(&tester); \
})
struct expect_msg {
const char *substr; /* substring match */
regex_t regex;
bool is_regex;
bool on_next_line;
bool negative;
};
struct expected_msgs {
struct expect_msg *patterns;
size_t cnt;
};
void validate_msgs(const char *log_buf, struct expected_msgs *msgs,
void (*emit_fn)(const char *buf, bool force));
#endif /* __TEST_PROGS_H */