918 lines
23 KiB
C
918 lines
23 KiB
C
|
// SPDX-License-Identifier: GPL-2.0
|
||
|
|
||
|
#include <limits.h>
|
||
|
#include <stdio.h>
|
||
|
#include <string.h>
|
||
|
#include <ctype.h>
|
||
|
#include <regex.h>
|
||
|
#include <test_progs.h>
|
||
|
|
||
|
#include "bpf/btf.h"
|
||
|
#include "bpf_util.h"
|
||
|
#include "linux/filter.h"
|
||
|
#include "disasm.h"
|
||
|
|
||
|
#define MAX_PROG_TEXT_SZ (32 * 1024)
|
||
|
|
||
|
/* The code in this file serves the sole purpose of executing test cases
|
||
|
* specified in the test_cases array. Each test case specifies a program
|
||
|
* type, context field offset, and disassembly patterns that correspond
|
||
|
* to read and write instructions generated by
|
||
|
* verifier.c:convert_ctx_access() for accessing that field.
|
||
|
*
|
||
|
* For each test case, up to three programs are created:
|
||
|
* - One that uses BPF_LDX_MEM to read the context field.
|
||
|
* - One that uses BPF_STX_MEM to write to the context field.
|
||
|
* - One that uses BPF_ST_MEM to write to the context field.
|
||
|
*
|
||
|
* The disassembly of each program is then compared with the pattern
|
||
|
* specified in the test case.
|
||
|
*/
|
||
|
struct test_case {
|
||
|
char *name;
|
||
|
enum bpf_prog_type prog_type;
|
||
|
enum bpf_attach_type expected_attach_type;
|
||
|
int field_offset;
|
||
|
int field_sz;
|
||
|
/* Program generated for BPF_ST_MEM uses value 42 by default,
|
||
|
* this field allows to specify custom value.
|
||
|
*/
|
||
|
struct {
|
||
|
bool use;
|
||
|
int value;
|
||
|
} st_value;
|
||
|
/* Pattern for BPF_LDX_MEM(field_sz, dst, ctx, field_offset) */
|
||
|
char *read;
|
||
|
/* Pattern for BPF_STX_MEM(field_sz, ctx, src, field_offset) and
|
||
|
* BPF_ST_MEM (field_sz, ctx, src, field_offset)
|
||
|
*/
|
||
|
char *write;
|
||
|
/* Pattern for BPF_ST_MEM(field_sz, ctx, src, field_offset),
|
||
|
* takes priority over `write`.
|
||
|
*/
|
||
|
char *write_st;
|
||
|
/* Pattern for BPF_STX_MEM (field_sz, ctx, src, field_offset),
|
||
|
* takes priority over `write`.
|
||
|
*/
|
||
|
char *write_stx;
|
||
|
};
|
||
|
|
||
|
#define N(_prog_type, type, field, name_extra...) \
|
||
|
.name = #_prog_type "." #field name_extra, \
|
||
|
.prog_type = BPF_PROG_TYPE_##_prog_type, \
|
||
|
.field_offset = offsetof(type, field), \
|
||
|
.field_sz = sizeof(typeof(((type *)NULL)->field))
|
||
|
|
||
|
static struct test_case test_cases[] = {
|
||
|
/* Sign extension on s390 changes the pattern */
|
||
|
#if defined(__x86_64__) || defined(__aarch64__)
|
||
|
{
|
||
|
N(SCHED_CLS, struct __sk_buff, tstamp),
|
||
|
.read = "r11 = *(u8 *)($ctx + sk_buff::__mono_tc_offset);"
|
||
|
"w11 &= 3;"
|
||
|
"if w11 != 0x3 goto pc+2;"
|
||
|
"$dst = 0;"
|
||
|
"goto pc+1;"
|
||
|
"$dst = *(u64 *)($ctx + sk_buff::tstamp);",
|
||
|
.write = "r11 = *(u8 *)($ctx + sk_buff::__mono_tc_offset);"
|
||
|
"if w11 & 0x2 goto pc+1;"
|
||
|
"goto pc+2;"
|
||
|
"w11 &= -2;"
|
||
|
"*(u8 *)($ctx + sk_buff::__mono_tc_offset) = r11;"
|
||
|
"*(u64 *)($ctx + sk_buff::tstamp) = $src;",
|
||
|
},
|
||
|
#endif
|
||
|
{
|
||
|
N(SCHED_CLS, struct __sk_buff, priority),
|
||
|
.read = "$dst = *(u32 *)($ctx + sk_buff::priority);",
|
||
|
.write = "*(u32 *)($ctx + sk_buff::priority) = $src;",
|
||
|
},
|
||
|
{
|
||
|
N(SCHED_CLS, struct __sk_buff, mark),
|
||
|
.read = "$dst = *(u32 *)($ctx + sk_buff::mark);",
|
||
|
.write = "*(u32 *)($ctx + sk_buff::mark) = $src;",
|
||
|
},
|
||
|
{
|
||
|
N(SCHED_CLS, struct __sk_buff, cb[0]),
|
||
|
.read = "$dst = *(u32 *)($ctx + $(sk_buff::cb + qdisc_skb_cb::data));",
|
||
|
.write = "*(u32 *)($ctx + $(sk_buff::cb + qdisc_skb_cb::data)) = $src;",
|
||
|
},
|
||
|
{
|
||
|
N(SCHED_CLS, struct __sk_buff, tc_classid),
|
||
|
.read = "$dst = *(u16 *)($ctx + $(sk_buff::cb + qdisc_skb_cb::tc_classid));",
|
||
|
.write = "*(u16 *)($ctx + $(sk_buff::cb + qdisc_skb_cb::tc_classid)) = $src;",
|
||
|
},
|
||
|
{
|
||
|
N(SCHED_CLS, struct __sk_buff, tc_index),
|
||
|
.read = "$dst = *(u16 *)($ctx + sk_buff::tc_index);",
|
||
|
.write = "*(u16 *)($ctx + sk_buff::tc_index) = $src;",
|
||
|
},
|
||
|
{
|
||
|
N(SCHED_CLS, struct __sk_buff, queue_mapping),
|
||
|
.read = "$dst = *(u16 *)($ctx + sk_buff::queue_mapping);",
|
||
|
.write_stx = "if $src >= 0xffff goto pc+1;"
|
||
|
"*(u16 *)($ctx + sk_buff::queue_mapping) = $src;",
|
||
|
.write_st = "*(u16 *)($ctx + sk_buff::queue_mapping) = $src;",
|
||
|
},
|
||
|
{
|
||
|
/* This is a corner case in filter.c:bpf_convert_ctx_access() */
|
||
|
N(SCHED_CLS, struct __sk_buff, queue_mapping, ".ushrt_max"),
|
||
|
.st_value = { true, USHRT_MAX },
|
||
|
.write_st = "goto pc+0;",
|
||
|
},
|
||
|
{
|
||
|
N(CGROUP_SOCK, struct bpf_sock, bound_dev_if),
|
||
|
.read = "$dst = *(u32 *)($ctx + sock_common::skc_bound_dev_if);",
|
||
|
.write = "*(u32 *)($ctx + sock_common::skc_bound_dev_if) = $src;",
|
||
|
},
|
||
|
{
|
||
|
N(CGROUP_SOCK, struct bpf_sock, mark),
|
||
|
.read = "$dst = *(u32 *)($ctx + sock::sk_mark);",
|
||
|
.write = "*(u32 *)($ctx + sock::sk_mark) = $src;",
|
||
|
},
|
||
|
{
|
||
|
N(CGROUP_SOCK, struct bpf_sock, priority),
|
||
|
.read = "$dst = *(u32 *)($ctx + sock::sk_priority);",
|
||
|
.write = "*(u32 *)($ctx + sock::sk_priority) = $src;",
|
||
|
},
|
||
|
{
|
||
|
N(SOCK_OPS, struct bpf_sock_ops, replylong[0]),
|
||
|
.read = "$dst = *(u32 *)($ctx + bpf_sock_ops_kern::replylong);",
|
||
|
.write = "*(u32 *)($ctx + bpf_sock_ops_kern::replylong) = $src;",
|
||
|
},
|
||
|
{
|
||
|
N(CGROUP_SYSCTL, struct bpf_sysctl, file_pos),
|
||
|
#if __BYTE_ORDER == __LITTLE_ENDIAN
|
||
|
.read = "$dst = *(u64 *)($ctx + bpf_sysctl_kern::ppos);"
|
||
|
"$dst = *(u32 *)($dst +0);",
|
||
|
.write = "*(u64 *)($ctx + bpf_sysctl_kern::tmp_reg) = r9;"
|
||
|
"r9 = *(u64 *)($ctx + bpf_sysctl_kern::ppos);"
|
||
|
"*(u32 *)(r9 +0) = $src;"
|
||
|
"r9 = *(u64 *)($ctx + bpf_sysctl_kern::tmp_reg);",
|
||
|
#else
|
||
|
.read = "$dst = *(u64 *)($ctx + bpf_sysctl_kern::ppos);"
|
||
|
"$dst = *(u32 *)($dst +4);",
|
||
|
.write = "*(u64 *)($ctx + bpf_sysctl_kern::tmp_reg) = r9;"
|
||
|
"r9 = *(u64 *)($ctx + bpf_sysctl_kern::ppos);"
|
||
|
"*(u32 *)(r9 +4) = $src;"
|
||
|
"r9 = *(u64 *)($ctx + bpf_sysctl_kern::tmp_reg);",
|
||
|
#endif
|
||
|
},
|
||
|
{
|
||
|
N(CGROUP_SOCKOPT, struct bpf_sockopt, sk),
|
||
|
.read = "$dst = *(u64 *)($ctx + bpf_sockopt_kern::sk);",
|
||
|
.expected_attach_type = BPF_CGROUP_GETSOCKOPT,
|
||
|
},
|
||
|
{
|
||
|
N(CGROUP_SOCKOPT, struct bpf_sockopt, level),
|
||
|
.read = "$dst = *(u32 *)($ctx + bpf_sockopt_kern::level);",
|
||
|
.write = "*(u32 *)($ctx + bpf_sockopt_kern::level) = $src;",
|
||
|
.expected_attach_type = BPF_CGROUP_SETSOCKOPT,
|
||
|
},
|
||
|
{
|
||
|
N(CGROUP_SOCKOPT, struct bpf_sockopt, optname),
|
||
|
.read = "$dst = *(u32 *)($ctx + bpf_sockopt_kern::optname);",
|
||
|
.write = "*(u32 *)($ctx + bpf_sockopt_kern::optname) = $src;",
|
||
|
.expected_attach_type = BPF_CGROUP_SETSOCKOPT,
|
||
|
},
|
||
|
{
|
||
|
N(CGROUP_SOCKOPT, struct bpf_sockopt, optlen),
|
||
|
.read = "$dst = *(u32 *)($ctx + bpf_sockopt_kern::optlen);",
|
||
|
.write = "*(u32 *)($ctx + bpf_sockopt_kern::optlen) = $src;",
|
||
|
.expected_attach_type = BPF_CGROUP_SETSOCKOPT,
|
||
|
},
|
||
|
{
|
||
|
N(CGROUP_SOCKOPT, struct bpf_sockopt, retval),
|
||
|
.read = "$dst = *(u64 *)($ctx + bpf_sockopt_kern::current_task);"
|
||
|
"$dst = *(u64 *)($dst + task_struct::bpf_ctx);"
|
||
|
"$dst = *(u32 *)($dst + bpf_cg_run_ctx::retval);",
|
||
|
.write = "*(u64 *)($ctx + bpf_sockopt_kern::tmp_reg) = r9;"
|
||
|
"r9 = *(u64 *)($ctx + bpf_sockopt_kern::current_task);"
|
||
|
"r9 = *(u64 *)(r9 + task_struct::bpf_ctx);"
|
||
|
"*(u32 *)(r9 + bpf_cg_run_ctx::retval) = $src;"
|
||
|
"r9 = *(u64 *)($ctx + bpf_sockopt_kern::tmp_reg);",
|
||
|
.expected_attach_type = BPF_CGROUP_GETSOCKOPT,
|
||
|
},
|
||
|
{
|
||
|
N(CGROUP_SOCKOPT, struct bpf_sockopt, optval),
|
||
|
.read = "$dst = *(u64 *)($ctx + bpf_sockopt_kern::optval);",
|
||
|
.expected_attach_type = BPF_CGROUP_GETSOCKOPT,
|
||
|
},
|
||
|
{
|
||
|
N(CGROUP_SOCKOPT, struct bpf_sockopt, optval_end),
|
||
|
.read = "$dst = *(u64 *)($ctx + bpf_sockopt_kern::optval_end);",
|
||
|
.expected_attach_type = BPF_CGROUP_GETSOCKOPT,
|
||
|
},
|
||
|
};
|
||
|
|
||
|
#undef N
|
||
|
|
||
|
static regex_t *ident_regex;
|
||
|
static regex_t *field_regex;
|
||
|
|
||
|
static char *skip_space(char *str)
|
||
|
{
|
||
|
while (*str && isspace(*str))
|
||
|
++str;
|
||
|
return str;
|
||
|
}
|
||
|
|
||
|
static char *skip_space_and_semi(char *str)
|
||
|
{
|
||
|
while (*str && (isspace(*str) || *str == ';'))
|
||
|
++str;
|
||
|
return str;
|
||
|
}
|
||
|
|
||
|
static char *match_str(char *str, char *prefix)
|
||
|
{
|
||
|
while (*str && *prefix && *str == *prefix) {
|
||
|
++str;
|
||
|
++prefix;
|
||
|
}
|
||
|
if (*prefix)
|
||
|
return NULL;
|
||
|
return str;
|
||
|
}
|
||
|
|
||
|
static char *match_number(char *str, int num)
|
||
|
{
|
||
|
char *next;
|
||
|
int snum = strtol(str, &next, 10);
|
||
|
|
||
|
if (next - str == 0 || num != snum)
|
||
|
return NULL;
|
||
|
|
||
|
return next;
|
||
|
}
|
||
|
|
||
|
static int find_field_offset_aux(struct btf *btf, int btf_id, char *field_name, int off)
|
||
|
{
|
||
|
const struct btf_type *type = btf__type_by_id(btf, btf_id);
|
||
|
const struct btf_member *m;
|
||
|
__u16 mnum;
|
||
|
int i;
|
||
|
|
||
|
if (!type) {
|
||
|
PRINT_FAIL("Can't find btf_type for id %d\n", btf_id);
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
if (!btf_is_struct(type) && !btf_is_union(type)) {
|
||
|
PRINT_FAIL("BTF id %d is not struct or union\n", btf_id);
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
m = btf_members(type);
|
||
|
mnum = btf_vlen(type);
|
||
|
|
||
|
for (i = 0; i < mnum; ++i, ++m) {
|
||
|
const char *mname = btf__name_by_offset(btf, m->name_off);
|
||
|
|
||
|
if (strcmp(mname, "") == 0) {
|
||
|
int msize = find_field_offset_aux(btf, m->type, field_name,
|
||
|
off + m->offset);
|
||
|
if (msize >= 0)
|
||
|
return msize;
|
||
|
}
|
||
|
|
||
|
if (strcmp(mname, field_name))
|
||
|
continue;
|
||
|
|
||
|
return (off + m->offset) / 8;
|
||
|
}
|
||
|
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
static int find_field_offset(struct btf *btf, char *pattern, regmatch_t *matches)
|
||
|
{
|
||
|
int type_sz = matches[1].rm_eo - matches[1].rm_so;
|
||
|
int field_sz = matches[2].rm_eo - matches[2].rm_so;
|
||
|
char *type = pattern + matches[1].rm_so;
|
||
|
char *field = pattern + matches[2].rm_so;
|
||
|
char field_str[128] = {};
|
||
|
char type_str[128] = {};
|
||
|
int btf_id, field_offset;
|
||
|
|
||
|
if (type_sz >= sizeof(type_str)) {
|
||
|
PRINT_FAIL("Malformed pattern: type ident is too long: %d\n", type_sz);
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
if (field_sz >= sizeof(field_str)) {
|
||
|
PRINT_FAIL("Malformed pattern: field ident is too long: %d\n", field_sz);
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
strncpy(type_str, type, type_sz);
|
||
|
strncpy(field_str, field, field_sz);
|
||
|
btf_id = btf__find_by_name(btf, type_str);
|
||
|
if (btf_id < 0) {
|
||
|
PRINT_FAIL("No BTF info for type %s\n", type_str);
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
field_offset = find_field_offset_aux(btf, btf_id, field_str, 0);
|
||
|
if (field_offset < 0) {
|
||
|
PRINT_FAIL("No BTF info for field %s::%s\n", type_str, field_str);
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
return field_offset;
|
||
|
}
|
||
|
|
||
|
static regex_t *compile_regex(char *pat)
|
||
|
{
|
||
|
regex_t *re;
|
||
|
int err;
|
||
|
|
||
|
re = malloc(sizeof(regex_t));
|
||
|
if (!re) {
|
||
|
PRINT_FAIL("Can't alloc regex\n");
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
err = regcomp(re, pat, REG_EXTENDED);
|
||
|
if (err) {
|
||
|
char errbuf[512];
|
||
|
|
||
|
regerror(err, re, errbuf, sizeof(errbuf));
|
||
|
PRINT_FAIL("Can't compile regex: %s\n", errbuf);
|
||
|
free(re);
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
return re;
|
||
|
}
|
||
|
|
||
|
static void free_regex(regex_t *re)
|
||
|
{
|
||
|
if (!re)
|
||
|
return;
|
||
|
|
||
|
regfree(re);
|
||
|
free(re);
|
||
|
}
|
||
|
|
||
|
static u32 max_line_len(char *str)
|
||
|
{
|
||
|
u32 max_line = 0;
|
||
|
char *next = str;
|
||
|
|
||
|
while (next) {
|
||
|
next = strchr(str, '\n');
|
||
|
if (next) {
|
||
|
max_line = max_t(u32, max_line, (next - str));
|
||
|
str = next + 1;
|
||
|
} else {
|
||
|
max_line = max_t(u32, max_line, strlen(str));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return min(max_line, 60u);
|
||
|
}
|
||
|
|
||
|
/* Print strings `pattern_origin` and `text_origin` side by side,
|
||
|
* assume `pattern_pos` and `text_pos` designate location within
|
||
|
* corresponding origin string where match diverges.
|
||
|
* The output should look like:
|
||
|
*
|
||
|
* Can't match disassembly(left) with pattern(right):
|
||
|
* r2 = *(u64 *)(r1 +0) ; $dst = *(u64 *)($ctx + bpf_sockopt_kern::sk1)
|
||
|
* ^ ^
|
||
|
* r0 = 0 ;
|
||
|
* exit ;
|
||
|
*/
|
||
|
static void print_match_error(FILE *out,
|
||
|
char *pattern_origin, char *text_origin,
|
||
|
char *pattern_pos, char *text_pos)
|
||
|
{
|
||
|
char *pattern = pattern_origin;
|
||
|
char *text = text_origin;
|
||
|
int middle = max_line_len(text) + 2;
|
||
|
|
||
|
fprintf(out, "Can't match disassembly(left) with pattern(right):\n");
|
||
|
while (*pattern || *text) {
|
||
|
int column = 0;
|
||
|
int mark1 = -1;
|
||
|
int mark2 = -1;
|
||
|
|
||
|
/* Print one line from text */
|
||
|
while (*text && *text != '\n') {
|
||
|
if (text == text_pos)
|
||
|
mark1 = column;
|
||
|
fputc(*text, out);
|
||
|
++text;
|
||
|
++column;
|
||
|
}
|
||
|
if (text == text_pos)
|
||
|
mark1 = column;
|
||
|
|
||
|
/* Pad to the middle */
|
||
|
while (column < middle) {
|
||
|
fputc(' ', out);
|
||
|
++column;
|
||
|
}
|
||
|
fputs("; ", out);
|
||
|
column += 3;
|
||
|
|
||
|
/* Print one line from pattern, pattern lines are terminated by ';' */
|
||
|
while (*pattern && *pattern != ';') {
|
||
|
if (pattern == pattern_pos)
|
||
|
mark2 = column;
|
||
|
fputc(*pattern, out);
|
||
|
++pattern;
|
||
|
++column;
|
||
|
}
|
||
|
if (pattern == pattern_pos)
|
||
|
mark2 = column;
|
||
|
|
||
|
fputc('\n', out);
|
||
|
if (*pattern)
|
||
|
++pattern;
|
||
|
if (*text)
|
||
|
++text;
|
||
|
|
||
|
/* If pattern and text diverge at this line, print an
|
||
|
* additional line with '^' marks, highlighting
|
||
|
* positions where match fails.
|
||
|
*/
|
||
|
if (mark1 > 0 || mark2 > 0) {
|
||
|
for (column = 0; column <= max(mark1, mark2); ++column) {
|
||
|
if (column == mark1 || column == mark2)
|
||
|
fputc('^', out);
|
||
|
else
|
||
|
fputc(' ', out);
|
||
|
}
|
||
|
fputc('\n', out);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* Test if `text` matches `pattern`. Pattern consists of the following elements:
|
||
|
*
|
||
|
* - Field offset references:
|
||
|
*
|
||
|
* <type>::<field>
|
||
|
*
|
||
|
* When such reference is encountered BTF is used to compute numerical
|
||
|
* value for the offset of <field> in <type>. The `text` is expected to
|
||
|
* contain matching numerical value.
|
||
|
*
|
||
|
* - Field groups:
|
||
|
*
|
||
|
* $(<type>::<field> [+ <type>::<field>]*)
|
||
|
*
|
||
|
* Allows to specify an offset that is a sum of multiple field offsets.
|
||
|
* The `text` is expected to contain matching numerical value.
|
||
|
*
|
||
|
* - Variable references, e.g. `$src`, `$dst`, `$ctx`.
|
||
|
* These are substitutions specified in `reg_map` array.
|
||
|
* If a substring of pattern is equal to `reg_map[i][0]` the `text` is
|
||
|
* expected to contain `reg_map[i][1]` in the matching position.
|
||
|
*
|
||
|
* - Whitespace is ignored, ';' counts as whitespace for `pattern`.
|
||
|
*
|
||
|
* - Any other characters, `pattern` and `text` should match one-to-one.
|
||
|
*
|
||
|
* Example of a pattern:
|
||
|
*
|
||
|
* __________ fields group ________________
|
||
|
* ' '
|
||
|
* *(u16 *)($ctx + $(sk_buff::cb + qdisc_skb_cb::tc_classid)) = $src;
|
||
|
* ^^^^ '______________________'
|
||
|
* variable reference field offset reference
|
||
|
*/
|
||
|
static bool match_pattern(struct btf *btf, char *pattern, char *text, char *reg_map[][2])
|
||
|
{
|
||
|
char *pattern_origin = pattern;
|
||
|
char *text_origin = text;
|
||
|
regmatch_t matches[3];
|
||
|
|
||
|
_continue:
|
||
|
while (*pattern) {
|
||
|
if (!*text)
|
||
|
goto err;
|
||
|
|
||
|
/* Skip whitespace */
|
||
|
if (isspace(*pattern) || *pattern == ';') {
|
||
|
if (!isspace(*text) && text != text_origin && isalnum(text[-1]))
|
||
|
goto err;
|
||
|
pattern = skip_space_and_semi(pattern);
|
||
|
text = skip_space(text);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
/* Check for variable references */
|
||
|
for (int i = 0; reg_map[i][0]; ++i) {
|
||
|
char *pattern_next, *text_next;
|
||
|
|
||
|
pattern_next = match_str(pattern, reg_map[i][0]);
|
||
|
if (!pattern_next)
|
||
|
continue;
|
||
|
|
||
|
text_next = match_str(text, reg_map[i][1]);
|
||
|
if (!text_next)
|
||
|
goto err;
|
||
|
|
||
|
pattern = pattern_next;
|
||
|
text = text_next;
|
||
|
goto _continue;
|
||
|
}
|
||
|
|
||
|
/* Match field group:
|
||
|
* $(sk_buff::cb + qdisc_skb_cb::tc_classid)
|
||
|
*/
|
||
|
if (strncmp(pattern, "$(", 2) == 0) {
|
||
|
char *group_start = pattern, *text_next;
|
||
|
int acc_offset = 0;
|
||
|
|
||
|
pattern += 2;
|
||
|
|
||
|
for (;;) {
|
||
|
int field_offset;
|
||
|
|
||
|
pattern = skip_space(pattern);
|
||
|
if (!*pattern) {
|
||
|
PRINT_FAIL("Unexpected end of pattern\n");
|
||
|
goto err;
|
||
|
}
|
||
|
|
||
|
if (*pattern == ')') {
|
||
|
++pattern;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (*pattern == '+') {
|
||
|
++pattern;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
printf("pattern: %s\n", pattern);
|
||
|
if (regexec(field_regex, pattern, 3, matches, 0) != 0) {
|
||
|
PRINT_FAIL("Field reference expected\n");
|
||
|
goto err;
|
||
|
}
|
||
|
|
||
|
field_offset = find_field_offset(btf, pattern, matches);
|
||
|
if (field_offset < 0)
|
||
|
goto err;
|
||
|
|
||
|
pattern += matches[0].rm_eo;
|
||
|
acc_offset += field_offset;
|
||
|
}
|
||
|
|
||
|
text_next = match_number(text, acc_offset);
|
||
|
if (!text_next) {
|
||
|
PRINT_FAIL("No match for group offset %.*s (%d)\n",
|
||
|
(int)(pattern - group_start),
|
||
|
group_start,
|
||
|
acc_offset);
|
||
|
goto err;
|
||
|
}
|
||
|
text = text_next;
|
||
|
}
|
||
|
|
||
|
/* Match field reference:
|
||
|
* sk_buff::cb
|
||
|
*/
|
||
|
if (regexec(field_regex, pattern, 3, matches, 0) == 0) {
|
||
|
int field_offset;
|
||
|
char *text_next;
|
||
|
|
||
|
field_offset = find_field_offset(btf, pattern, matches);
|
||
|
if (field_offset < 0)
|
||
|
goto err;
|
||
|
|
||
|
text_next = match_number(text, field_offset);
|
||
|
if (!text_next) {
|
||
|
PRINT_FAIL("No match for field offset %.*s (%d)\n",
|
||
|
(int)matches[0].rm_eo, pattern, field_offset);
|
||
|
goto err;
|
||
|
}
|
||
|
|
||
|
pattern += matches[0].rm_eo;
|
||
|
text = text_next;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
/* If pattern points to identifier not followed by '::'
|
||
|
* skip the identifier to avoid n^2 application of the
|
||
|
* field reference rule.
|
||
|
*/
|
||
|
if (regexec(ident_regex, pattern, 1, matches, 0) == 0) {
|
||
|
if (strncmp(pattern, text, matches[0].rm_eo) != 0)
|
||
|
goto err;
|
||
|
|
||
|
pattern += matches[0].rm_eo;
|
||
|
text += matches[0].rm_eo;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
/* Match literally */
|
||
|
if (*pattern != *text)
|
||
|
goto err;
|
||
|
|
||
|
++pattern;
|
||
|
++text;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
|
||
|
err:
|
||
|
test__fail();
|
||
|
print_match_error(stdout, pattern_origin, text_origin, pattern, text);
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/* Request BPF program instructions after all rewrites are applied,
|
||
|
* e.g. verifier.c:convert_ctx_access() is done.
|
||
|
*/
|
||
|
static int get_xlated_program(int fd_prog, struct bpf_insn **buf, __u32 *cnt)
|
||
|
{
|
||
|
struct bpf_prog_info info = {};
|
||
|
__u32 info_len = sizeof(info);
|
||
|
__u32 xlated_prog_len;
|
||
|
__u32 buf_element_size = sizeof(struct bpf_insn);
|
||
|
|
||
|
if (bpf_prog_get_info_by_fd(fd_prog, &info, &info_len)) {
|
||
|
perror("bpf_prog_get_info_by_fd failed");
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
xlated_prog_len = info.xlated_prog_len;
|
||
|
if (xlated_prog_len % buf_element_size) {
|
||
|
printf("Program length %d is not multiple of %d\n",
|
||
|
xlated_prog_len, buf_element_size);
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
*cnt = xlated_prog_len / buf_element_size;
|
||
|
*buf = calloc(*cnt, buf_element_size);
|
||
|
if (!buf) {
|
||
|
perror("can't allocate xlated program buffer");
|
||
|
return -ENOMEM;
|
||
|
}
|
||
|
|
||
|
bzero(&info, sizeof(info));
|
||
|
info.xlated_prog_len = xlated_prog_len;
|
||
|
info.xlated_prog_insns = (__u64)(unsigned long)*buf;
|
||
|
if (bpf_prog_get_info_by_fd(fd_prog, &info, &info_len)) {
|
||
|
perror("second bpf_prog_get_info_by_fd failed");
|
||
|
goto out_free_buf;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
|
||
|
out_free_buf:
|
||
|
free(*buf);
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
static void print_insn(void *private_data, const char *fmt, ...)
|
||
|
{
|
||
|
va_list args;
|
||
|
|
||
|
va_start(args, fmt);
|
||
|
vfprintf((FILE *)private_data, fmt, args);
|
||
|
va_end(args);
|
||
|
}
|
||
|
|
||
|
/* Disassemble instructions to a stream */
|
||
|
static void print_xlated(FILE *out, struct bpf_insn *insn, __u32 len)
|
||
|
{
|
||
|
const struct bpf_insn_cbs cbs = {
|
||
|
.cb_print = print_insn,
|
||
|
.cb_call = NULL,
|
||
|
.cb_imm = NULL,
|
||
|
.private_data = out,
|
||
|
};
|
||
|
bool double_insn = false;
|
||
|
int i;
|
||
|
|
||
|
for (i = 0; i < len; i++) {
|
||
|
if (double_insn) {
|
||
|
double_insn = false;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
double_insn = insn[i].code == (BPF_LD | BPF_IMM | BPF_DW);
|
||
|
print_bpf_insn(&cbs, insn + i, true);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* We share code with kernel BPF disassembler, it adds '(FF) ' prefix
|
||
|
* for each instruction (FF stands for instruction `code` byte).
|
||
|
* This function removes the prefix inplace for each line in `str`.
|
||
|
*/
|
||
|
static void remove_insn_prefix(char *str, int size)
|
||
|
{
|
||
|
const int prefix_size = 5;
|
||
|
|
||
|
int write_pos = 0, read_pos = prefix_size;
|
||
|
int len = strlen(str);
|
||
|
char c;
|
||
|
|
||
|
size = min(size, len);
|
||
|
|
||
|
while (read_pos < size) {
|
||
|
c = str[read_pos++];
|
||
|
if (c == 0)
|
||
|
break;
|
||
|
str[write_pos++] = c;
|
||
|
if (c == '\n')
|
||
|
read_pos += prefix_size;
|
||
|
}
|
||
|
str[write_pos] = 0;
|
||
|
}
|
||
|
|
||
|
struct prog_info {
|
||
|
char *prog_kind;
|
||
|
enum bpf_prog_type prog_type;
|
||
|
enum bpf_attach_type expected_attach_type;
|
||
|
struct bpf_insn *prog;
|
||
|
u32 prog_len;
|
||
|
};
|
||
|
|
||
|
static void match_program(struct btf *btf,
|
||
|
struct prog_info *pinfo,
|
||
|
char *pattern,
|
||
|
char *reg_map[][2],
|
||
|
bool skip_first_insn)
|
||
|
{
|
||
|
struct bpf_insn *buf = NULL;
|
||
|
int err = 0, prog_fd = 0;
|
||
|
FILE *prog_out = NULL;
|
||
|
char *text = NULL;
|
||
|
__u32 cnt = 0;
|
||
|
|
||
|
text = calloc(MAX_PROG_TEXT_SZ, 1);
|
||
|
if (!text) {
|
||
|
PRINT_FAIL("Can't allocate %d bytes\n", MAX_PROG_TEXT_SZ);
|
||
|
goto out;
|
||
|
}
|
||
|
|
||
|
// TODO: log level
|
||
|
LIBBPF_OPTS(bpf_prog_load_opts, opts);
|
||
|
opts.log_buf = text;
|
||
|
opts.log_size = MAX_PROG_TEXT_SZ;
|
||
|
opts.log_level = 1 | 2 | 4;
|
||
|
opts.expected_attach_type = pinfo->expected_attach_type;
|
||
|
|
||
|
prog_fd = bpf_prog_load(pinfo->prog_type, NULL, "GPL",
|
||
|
pinfo->prog, pinfo->prog_len, &opts);
|
||
|
if (prog_fd < 0) {
|
||
|
PRINT_FAIL("Can't load program, errno %d (%s), verifier log:\n%s\n",
|
||
|
errno, strerror(errno), text);
|
||
|
goto out;
|
||
|
}
|
||
|
|
||
|
memset(text, 0, MAX_PROG_TEXT_SZ);
|
||
|
|
||
|
err = get_xlated_program(prog_fd, &buf, &cnt);
|
||
|
if (err) {
|
||
|
PRINT_FAIL("Can't load back BPF program\n");
|
||
|
goto out;
|
||
|
}
|
||
|
|
||
|
prog_out = fmemopen(text, MAX_PROG_TEXT_SZ - 1, "w");
|
||
|
if (!prog_out) {
|
||
|
PRINT_FAIL("Can't open memory stream\n");
|
||
|
goto out;
|
||
|
}
|
||
|
if (skip_first_insn)
|
||
|
print_xlated(prog_out, buf + 1, cnt - 1);
|
||
|
else
|
||
|
print_xlated(prog_out, buf, cnt);
|
||
|
fclose(prog_out);
|
||
|
remove_insn_prefix(text, MAX_PROG_TEXT_SZ);
|
||
|
|
||
|
ASSERT_TRUE(match_pattern(btf, pattern, text, reg_map),
|
||
|
pinfo->prog_kind);
|
||
|
|
||
|
out:
|
||
|
if (prog_fd)
|
||
|
close(prog_fd);
|
||
|
free(buf);
|
||
|
free(text);
|
||
|
}
|
||
|
|
||
|
static void run_one_testcase(struct btf *btf, struct test_case *test)
|
||
|
{
|
||
|
struct prog_info pinfo = {};
|
||
|
int bpf_sz;
|
||
|
|
||
|
if (!test__start_subtest(test->name))
|
||
|
return;
|
||
|
|
||
|
switch (test->field_sz) {
|
||
|
case 8:
|
||
|
bpf_sz = BPF_DW;
|
||
|
break;
|
||
|
case 4:
|
||
|
bpf_sz = BPF_W;
|
||
|
break;
|
||
|
case 2:
|
||
|
bpf_sz = BPF_H;
|
||
|
break;
|
||
|
case 1:
|
||
|
bpf_sz = BPF_B;
|
||
|
break;
|
||
|
default:
|
||
|
PRINT_FAIL("Unexpected field size: %d, want 8,4,2 or 1\n", test->field_sz);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
pinfo.prog_type = test->prog_type;
|
||
|
pinfo.expected_attach_type = test->expected_attach_type;
|
||
|
|
||
|
if (test->read) {
|
||
|
struct bpf_insn ldx_prog[] = {
|
||
|
BPF_LDX_MEM(bpf_sz, BPF_REG_2, BPF_REG_1, test->field_offset),
|
||
|
BPF_MOV64_IMM(BPF_REG_0, 0),
|
||
|
BPF_EXIT_INSN(),
|
||
|
};
|
||
|
char *reg_map[][2] = {
|
||
|
{ "$ctx", "r1" },
|
||
|
{ "$dst", "r2" },
|
||
|
{}
|
||
|
};
|
||
|
|
||
|
pinfo.prog_kind = "LDX";
|
||
|
pinfo.prog = ldx_prog;
|
||
|
pinfo.prog_len = ARRAY_SIZE(ldx_prog);
|
||
|
match_program(btf, &pinfo, test->read, reg_map, false);
|
||
|
}
|
||
|
|
||
|
if (test->write || test->write_st || test->write_stx) {
|
||
|
struct bpf_insn stx_prog[] = {
|
||
|
BPF_MOV64_IMM(BPF_REG_2, 0),
|
||
|
BPF_STX_MEM(bpf_sz, BPF_REG_1, BPF_REG_2, test->field_offset),
|
||
|
BPF_MOV64_IMM(BPF_REG_0, 0),
|
||
|
BPF_EXIT_INSN(),
|
||
|
};
|
||
|
char *stx_reg_map[][2] = {
|
||
|
{ "$ctx", "r1" },
|
||
|
{ "$src", "r2" },
|
||
|
{}
|
||
|
};
|
||
|
struct bpf_insn st_prog[] = {
|
||
|
BPF_ST_MEM(bpf_sz, BPF_REG_1, test->field_offset,
|
||
|
test->st_value.use ? test->st_value.value : 42),
|
||
|
BPF_MOV64_IMM(BPF_REG_0, 0),
|
||
|
BPF_EXIT_INSN(),
|
||
|
};
|
||
|
char *st_reg_map[][2] = {
|
||
|
{ "$ctx", "r1" },
|
||
|
{ "$src", "42" },
|
||
|
{}
|
||
|
};
|
||
|
|
||
|
if (test->write || test->write_stx) {
|
||
|
char *pattern = test->write_stx ? test->write_stx : test->write;
|
||
|
|
||
|
pinfo.prog_kind = "STX";
|
||
|
pinfo.prog = stx_prog;
|
||
|
pinfo.prog_len = ARRAY_SIZE(stx_prog);
|
||
|
match_program(btf, &pinfo, pattern, stx_reg_map, true);
|
||
|
}
|
||
|
|
||
|
if (test->write || test->write_st) {
|
||
|
char *pattern = test->write_st ? test->write_st : test->write;
|
||
|
|
||
|
pinfo.prog_kind = "ST";
|
||
|
pinfo.prog = st_prog;
|
||
|
pinfo.prog_len = ARRAY_SIZE(st_prog);
|
||
|
match_program(btf, &pinfo, pattern, st_reg_map, false);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
test__end_subtest();
|
||
|
}
|
||
|
|
||
|
void test_ctx_rewrite(void)
|
||
|
{
|
||
|
struct btf *btf;
|
||
|
int i;
|
||
|
|
||
|
field_regex = compile_regex("^([[:alpha:]_][[:alnum:]_]+)::([[:alpha:]_][[:alnum:]_]+)");
|
||
|
ident_regex = compile_regex("^[[:alpha:]_][[:alnum:]_]+");
|
||
|
if (!field_regex || !ident_regex)
|
||
|
return;
|
||
|
|
||
|
btf = btf__load_vmlinux_btf();
|
||
|
if (!btf) {
|
||
|
PRINT_FAIL("Can't load vmlinux BTF, errno %d (%s)\n", errno, strerror(errno));
|
||
|
goto out;
|
||
|
}
|
||
|
|
||
|
for (i = 0; i < ARRAY_SIZE(test_cases); ++i)
|
||
|
run_one_testcase(btf, &test_cases[i]);
|
||
|
|
||
|
out:
|
||
|
btf__free(btf);
|
||
|
free_regex(field_regex);
|
||
|
free_regex(ident_regex);
|
||
|
}
|