// SPDX-License-Identifier: GPL-2.0-or-later
/* seitan - Syscall Expressive Interpreter, Transformer and Notifier
*
* cooker/match.c - Parse "match" rules from JSON recipe into bytecode
*
* Copyright 2023 Red Hat GmbH
* Author: Stefano Brivio <sbrivio@redhat.com>
*/
#include "parson.h"
#include "calls.h"
#include "cooker.h"
#include "gluten.h"
#include "emit.h"
#include "filter.h"
#include "parse.h"
#include "util.h"
#include "calls/net.h"
#include "seccomp_profile.h"
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/netfilter/nfnetlink.h>
#include <linux/netfilter/nf_tables.h>
#include <linux/netfilter/nf_tables_compat.h>
static struct num netlink_types[] = {
{ "newroute", RTM_NEWROUTE },
#define SUBSYS_NFCOMPAT(x) (NFNL_SUBSYS_NFT_COMPAT << 8 | (x))
{ "nf_compat_get", (SUBSYS_NFCOMPAT(NFNL_MSG_COMPAT_GET))},
#define SUBSYS_NFT(x) (NFNL_SUBSYS_NFTABLES << 8 | (x))
{ "nf_get_any",
SUBSYS_NFT(NFT_MSG_GETTABLE | NFT_MSG_GETCHAIN | NFT_MSG_GETRULE |
NFT_MSG_GETSET | NFT_MSG_GETSETELEM | NFT_MSG_GETGEN |
NFT_MSG_GETFLOWTABLE | NFT_MSG_GETOBJ) |
SUBSYS_NFCOMPAT(NFNL_MSG_COMPAT_GET) },
#undef SUBSYS_NFCOMPAT
{ "nf_newtable", SUBSYS_NFT(NFT_MSG_NEWTABLE) },
{ "nf_gettable", SUBSYS_NFT(NFT_MSG_GETTABLE) },
{ "nf_deltable", SUBSYS_NFT(NFT_MSG_DELTABLE) },
{ "nf_destroytable", SUBSYS_NFT(NFT_MSG_DESTROYTABLE) },
/* ignores ENOENT */
{ "nf_newchain", SUBSYS_NFT(NFT_MSG_NEWCHAIN) },
{ "nf_getchain", SUBSYS_NFT(NFT_MSG_GETCHAIN) },
{ "nf_delchain", SUBSYS_NFT(NFT_MSG_DELCHAIN) },
{ "nf_destroychain", SUBSYS_NFT(NFT_MSG_DESTROYCHAIN) },
{ "nf_newrule", SUBSYS_NFT(NFT_MSG_NEWRULE) },
{ "nf_getrule", SUBSYS_NFT(NFT_MSG_GETRULE) },
{ "nf_getrule_reset", SUBSYS_NFT(NFT_MSG_GETRULE_RESET) },
{ "nf_delrule", SUBSYS_NFT(NFT_MSG_DELRULE) },
{ "nf_destroyrule", SUBSYS_NFT(NFT_MSG_DESTROYRULE) },
{ "nf_newset", SUBSYS_NFT(NFT_MSG_NEWSET) },
{ "nf_getset", SUBSYS_NFT(NFT_MSG_GETSET) },
{ "nf_delset", SUBSYS_NFT(NFT_MSG_DELSET) },
{ "nf_destroyset", SUBSYS_NFT(NFT_MSG_DESTROYSET) },
{ "nf_newsetelem", SUBSYS_NFT(NFT_MSG_NEWSETELEM) },
{ "nf_getsetelem", SUBSYS_NFT(NFT_MSG_GETSETELEM) },
{ "nf_getsetelem_reset", SUBSYS_NFT(NFT_MSG_GETSETELEM_RESET) },
{ "nf_delsetelem", SUBSYS_NFT(NFT_MSG_DELSETELEM) },
{ "nf_destroysetelem", SUBSYS_NFT(NFT_MSG_DESTROYSETELEM) },
{ "nf_newgen", SUBSYS_NFT(NFT_MSG_NEWGEN) },
{ "nf_getgen", SUBSYS_NFT(NFT_MSG_GETGEN) },
{ "nf_trace", SUBSYS_NFT(NFT_MSG_TRACE) },
{ "nf_newobj", SUBSYS_NFT(NFT_MSG_NEWOBJ) },
{ "nf_getobj", SUBSYS_NFT(NFT_MSG_GETOBJ) },
{ "nf_getobj_reset", SUBSYS_NFT(NFT_MSG_GETOBJ_RESET) },
{ "nf_delobj", SUBSYS_NFT(NFT_MSG_DELOBJ) },
{ "nf_destroyobj", SUBSYS_NFT(NFT_MSG_DESTROYOBJ) },
{ "nf_newflowtable", SUBSYS_NFT(NFT_MSG_NEWFLOWTABLE) },
{ "nf_getflowtable", SUBSYS_NFT(NFT_MSG_GETFLOWTABLE) },
{ "nf_delflowtable", SUBSYS_NFT(NFT_MSG_DELFLOWTABLE) },
{ "nf_destroyflowtable", SUBSYS_NFT(NFT_MSG_DESTROYFLOWTABLE) },
#undef SUBSYS_NFT
{ 0 },
};
static struct num netlink_flags[] = {
{ "request", NLM_F_REQUEST },
{ "create", NLM_F_CREATE },
{ 0 },
};
static struct field netlink_header_fields[] = {
{
"type", USHORT, 0,
offsetof(struct nlmsghdr, nlmsg_type),
0, { .d_num = netlink_types }
},
{
"flags", USHORT, FLAGS,
offsetof(struct nlmsghdr, nlmsg_flags),
0, { .d_num = netlink_flags }
},
{ 0 },
};
static struct field netlink_header = {
"netlink", STRUCT, 0, 0, 0,
{ .d_struct = netlink_header_fields }
};
/**
* arg_load() - Allocate and build bytecode for one syscall argument
* @g: gluten context
* @a: Argument description from model
*
* Return: offset where (full) argument is stored
*/
static struct gluten_offset arg_load(struct gluten_ctx *g, struct arg *a)
{
struct gluten_offset offset;
int index = a->pos;
size_t size;
if (a->f.type == SELECTED) {
if (g->selected_arg[index]->f.type != UNDEF)
size = g->selected_arg[index]->f.size;
else
die(" no storage size for argument %s", a->f.name);
} else {
size = a->f.size;
}
if (!size || (a->f.flags & RBUF)) {
g->match_dst[index].offset.type = OFFSET_SECCOMP_DATA;
g->match_dst[index].offset.offset = index;
g->match_dst[index].len = 0;
return g->match_dst[index].offset;
}
filter_needs_deref();
if (g->match_dst[index].len) /* Already allocated */
return g->match_dst[index].offset;
offset = gluten_rw_alloc(g, size);
g->match_dst[index].offset = offset;
g->match_dst[index].len = size;
emit_load(g, offset, index, size);
return offset;
}
/**
* parse_field() - Parse generic field along with JSON value
* @g: gluten context
* @offset: Base offset of container field (actual offset for non-compound)
* @index: Index of parent syscall argument
* @f: Field from syscall model
* @jvalue: JSON value
*
* Return: parsed value for simple types, empty value otherwise
*/
static union value parse_field(struct gluten_ctx *g,
struct gluten_offset offset,
struct vec_desc *vec,
enum op_cmp_type cmp, enum jump_type jump,
int index, struct field *f, JSON_Value *jvalue)
{
struct gluten_offset const_offset, mask_offset, data_offset;
union value v = NO_VALUE, mask = NO_VALUE;
struct gluten_offset seccomp_offset;
struct field *f_inner;
const char *tag_name;
JSON_Object *tmp;
JSON_Array *set;
JSON_Value *sel;
size_t size;
if (f->name)
debug(" parsing field name %s", f->name);
/* Some types need pre-tagging preparation */
switch (f->type) {
case GNU_DEV_MAJOR:
/*
xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
______________________ _____________
*/
v.v_num = ((long long)0xfff << 44) | (0xfff << 8);
mask_offset = emit_data(g, U64, 0, &v);
offset = emit_bitwise(g, U64, NULL, BITWISE_AND, NULL_OFFSET,
offset, mask_offset);
break;
case GNU_DEV_MINOR:
/*
xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
_____________ ________
*/
v.v_num = 0xff | ((long long)0xfff << 20);
mask_offset = emit_data(g, U64, 0, &v);
offset = emit_bitwise(g, U64, NULL, BITWISE_AND, NULL_OFFSET,
offset, mask_offset);
break;
default:
break;
}
offset.offset += f->offset;
if (f->flags & IOV) {
struct gluten_offset iovlen = offset;
size_t alloc;
/* vectorised ops need a zero length descriptor at the end */
if (json_value_get_type(jvalue) == JSONObject &&
(tmp = json_value_get_object(jvalue)) &&
json_object_get_value(tmp, "netlink"))
alloc = f->size +
sizeof(((struct nlmsghdr *)NULL)->nlmsg_len);
else
alloc = f->size;
iovlen.offset += f->desc.d_iovlen;
offset = emit_iovload(g, offset, iovlen, alloc, f->size);
}
if (json_value_get_type(jvalue) == JSONObject &&
(tmp = json_value_get_object(jvalue)) &&
(tag_name = json_object_get_string(tmp, "set"))) {
debug(" setting tag reference '%s'", tag_name);
gluten_add_tag(g, tag_name, offset);
jvalue = json_object_get_value(tmp, "value");
}
if (!(f->flags & FLAGS) && /* For FLAGS, it's a single operation */
json_value_get_type(jvalue) == JSONObject &&
(tmp = json_value_get_object(jvalue)) &&
(set = json_object_get_array(tmp, "in"))) {
unsigned i, count = json_array_get_count(set);
if (cmp != CMP_NE || jump != JUMP_NEXT_BLOCK)
die("unsupported nested set");
for (i = 0; i < count; i++) {
if (i == count - 1) {
cmp = CMP_NE;
jump = JUMP_NEXT_BLOCK;
} else {
cmp = CMP_EQ;
jump = JUMP_NEXT_ACTION;
}
jvalue = json_array_get_value(set, i);
/* FIXME: ugly. Otherwise nested parse_field() will
* increment twice.
*/
offset.offset -= f->offset;
parse_field(g, offset, vec, cmp, jump, index, f,
jvalue);
offset.offset += f->offset;
}
return v; /* No SELECT based on sets... of course */
}
/* Nothing to match on: just store as reference */
if (!jvalue || (f->flags & RBUF))
return v;
switch (f->type) {
case USHORT:
case INT:
case LONG:
case U32:
data_offset = offset;
if ((f->flags & FLAGS) &&
json_value_get_type(jvalue) == JSONObject &&
(tmp = json_value_get_object(jvalue))) {
struct gluten_offset set_offset, cmp_offset, masked;
union value set, cmpterm;
value_get_flags(f->desc.d_num, tmp,
&set, &cmp, &cmpterm);
set_offset = emit_data(g, f->type, 0, &set);
masked = emit_bitwise(g, f->type, vec, BITWISE_AND,
NULL_OFFSET,
offset, set_offset);
cmp_offset = emit_data(g, f->type, 0, &cmpterm);
emit_cmp(g, cmp, vec, masked, cmp_offset,
gluten_size[f->type], jump);
emit_bpf_arg(index, f->type, cmpterm, set, cmp, g->mode);
break;
}
if (json_value_get_type(jvalue) == JSONArray) {
JSON_Array *array = json_value_get_array(jvalue);
unsigned i;
if (!(f->flags & FLAGS))
die("multiple values for non-FLAGS argument");
for (i = 0; i < json_array_get_count(array); i++) {
jvalue = json_array_get_value(array, i);
v.v_num |= value_get_num(f->desc.d_num, jvalue);
}
} else if (f->flags & MASK) {
mask.v_num = value_get_mask(f->desc.d_num);
mask_offset = emit_data(g, f->type, 0, &mask);
data_offset = emit_bitwise(g, f->type, vec, BITWISE_AND,
NULL_OFFSET, offset,
mask_offset);
if (vec)
vec->len_offset = 0;
v.v_num = value_get_num(f->desc.d_num, jvalue);
} else {
v.v_num = value_get_num(f->desc.d_num, jvalue);
}
const_offset = emit_data(g, f->type, 0, &v);
emit_cmp(g, cmp, vec, data_offset, const_offset,
gluten_size[f->type], jump);
emit_bpf_arg(index, f->type, v, mask, cmp, g->mode);
break;
case GNU_DEV_MAJOR:
/*
xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
______________________ _____________
*/
v.v_num = value_get_num(f->desc.d_num, jvalue);
v.v_num = (v.v_num & 0xfff) << 8 | (v.v_num & ~0xfff) << 32;
const_offset = emit_data(g, U64, 0, &v);
emit_cmp_field(g, cmp, vec, f, offset, const_offset, jump);
filter_needs_deref(); /* No shifts in BPF */
break;
case GNU_DEV_MINOR:
/*
xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx
____________________________ ________
*/
v.v_num = value_get_num(f->desc.d_num, jvalue);
v.v_num = (v.v_num & 0xff) | (v.v_num & ~0xfff) << 12;
const_offset = emit_data(g, U64, 0, &v);
emit_cmp_field(g, cmp, vec, f, offset, const_offset, jump);
filter_needs_deref(); /* No shifts in BPF */
break;
case SELECT:
f_inner = f->desc.d_select->field;
if ((tmp = json_value_get_object(jvalue))) {
if (!(sel = json_object_get_value(tmp, f_inner->name)))
die(" no selector for '%s'", f_inner->name);
} else {
sel = jvalue;
}
v = parse_field(g, offset, vec, cmp, jump, index, f_inner, sel);
f = select_field(g, index, f->desc.d_select, v);
if (f) {
parse_field(g, offset, vec, cmp, jump, index, f,
jvalue);
}
break;
case STRING:
if (json_value_get_type(jvalue) == JSONObject &&
(tmp = json_value_get_object(jvalue))) {
if ((jvalue = json_object_get_value(tmp, "netlink"))) {
struct vec_desc v_nl, *vecptr;
/* FIXME: even send()/sendto() might need
* vectorised operations
*/
if (f->flags & IOV) {
v_nl.start = offset;
v_nl.len_offset = offsetof(struct nlmsghdr,
nlmsg_len);
vecptr = &v_nl;
} else {
vecptr = NULL;
}
/* TODO: mark as CMPVEC, with:
* - offset of length (offsetof nlmsghdr)
* - vector pointer: that's already offset?
* - offset of comparison in vector (0 atm)
* - second term of comparison (from jvalue)
* - length (still needed?)
*
* plus BITWISEVEC? is it even needed?
*/
parse_field(g, offset, vecptr, cmp, jump, index,
&netlink_header, jvalue);
} else {
die(" unrecognised blob type");
}
break;
}
if ((v.v_str = json_value_get_string(jvalue)) == NULL)
die(" failed parsing field for value:%s",
json_serialize_to_string_pretty(jvalue));
if (strlen(v.v_str) + 1 > f->size)
die(" string %s too long for field", v.v_str);
const_offset = emit_data(g, STRING, strlen(v.v_str) + 1, &v);
emit_cmp(g, CMP_NE, vec, offset, const_offset,
strlen(v.v_str) + 1, JUMP_NEXT_BLOCK);
break;
case FDPATH:
case FDMOUNT:
v.v_str = json_value_get_string(jvalue);
size = strlen(v.v_str) + 1;
offset = gluten_rw_alloc(g, size);
const_offset = emit_data(g, STRING, size, &v);
seccomp_offset = emit_seccomp_data(index);
emit_resolvefd(g, f->type, seccomp_offset, offset, size);
emit_cmp(g, CMP_NE, vec, offset, const_offset,
size, JUMP_NEXT_BLOCK);
break;
case STRUCT:
for (f_inner = f->desc.d_struct; f_inner->name; f_inner++) {
JSON_Value *field_value;
tmp = json_value_get_object(jvalue);
field_value = json_object_get_value(tmp, f_inner->name);
if (!field_value)
continue;
parse_field(g, offset, vec, cmp, jump, index, f_inner,
field_value);
}
break;
default:
;
}
return v;
}
/**
* parse_arg() - Parse syscall argument from JSON, following model
* @g: gluten context
* @a: Argument description from model
* @value: JSON value for argument
*/
static void parse_arg(struct gluten_ctx *g, JSON_Value *jvalue, struct arg *a)
{
struct gluten_offset offset;
debug(" Parsing match argument %s", a->f.name);
offset = arg_load(g, a);
parse_field(g, offset, NULL, CMP_NE, JUMP_NEXT_BLOCK, a->pos, &a->f,
jvalue);
}
/**
* parse_match() - Parse one "match" item in syscall rules
* @g: gluten context
* @obj: Matching rule for one syscall
* @args: Description of arguments from syscall model
*/
static void parse_match(struct gluten_ctx *g, JSON_Object *obj,
struct arg *args)
{
unsigned count = 0;
struct arg *a;
for (a = args; a->f.name; a++) {
struct arg *real_arg = a;
JSON_Value *jvalue;
if (a->f.type == SELECTED) {
if (!(real_arg = g->selected_arg[a->pos]))
die(" No argument selected for %s", a->f.name);
}
if ((jvalue = json_object_get_value(obj, real_arg->f.name))) {
count++;
parse_arg(g, jvalue, real_arg);
}
}
if (count != json_object_get_count(obj))
die(" Stray elements in match");
}
/**
* handle_matches() - Parse "match" array, find syscall models
* @g: gluten context
* @value: "match" object containing array of rules
*/
void handle_matches(struct gluten_ctx *g, JSON_Value *value)
{
JSON_Array *matches = json_value_get_array(value);
unsigned i, count;
if (matches)
count = json_array_get_count(matches);
else
count = 1;
for (i = 0; i < count; i++) {
JSON_Object *match, *args;
struct call **set, *call;
const char *name;
if (matches)
match = json_array_get_object(matches, i);
else
match = json_value_get_object(value);
g->mr = g->ip;
name = json_object_get_name(match, 0);
args = json_object_get_object(match, name);
debug(" Parsing match %i: %s", i, name);
for (set = call_sets, call = set[0]; *set; ) {
if (!call->name) {
set++;
call = set[0];
continue;
}
if (!strcmp(name, call->name)) {
union value v = { .v_num = call->number };
debug(" Found description for %s", name);
emit_nr(g, emit_data(g, U64, 0, &v));
if (g->mode == SCMP_FILTER)
filter_notify(call->number);
else
scmp_profile_notify(call->name);
parse_match(g, args, call->args);
if (g->mode == SCMP_FILTER)
filter_flush_args(call->number);
else
scmp_profile_flush_args();
break;
}
call++;
}
if (!*set)
die(" Unknown system call: %s", name);
link_match(g);
}
link_matches(g);
}