Merge tag 'bpf-fixes' of git://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf

Pull bpf fixes from Alexei Starovoitov:

 - Fix effective prog array index with BPF_F_PREORDER (Amery Hung)

 - Zero-initialize the fib lookup flow struct (Avinash Duduskar)

 - Disable xfrm_decode_session hook attachment (Bradley Morgan)

 - Allow type tag BTF records to succeed other modifier records (Emil
   Tsalapatis)

 - Fix build_id caching in stack_map_get_build_id_offset() (Ihor
   Solodrai)

 - Add missing access_ok call to copy_user_syms (Jiri Olsa)

 - Fix stack slot index in nospec checks (Nuoqi Gui)

 - Preserve pointer spill metadata during half-slot cleanup (Nuoqi Gui)

 - Fix partial copy of non-linear test_run output (Sun Jian)

 - Fix BPF_PROG_ASSOC_STRUCT_OPS last field check (Thiébaud Weksteen)

 - Reset register bounds before narrowing retval range (Tristan Madani)

 - Fix vmlinux BTF leak in bpftool cgroup commands (Yichong Chen)

 - Guard error writes in conntrack kfuncs (Yiyang Chen)

* tag 'bpf-fixes' of git://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf:
  bpf: Disable xfrm_decode_session hook attachment
  selftests/bpf: Add test for stale bounds on LSM retval context load
  bpf: Reset register bounds before narrowing retval range in check_mem_access()
  selftests/bpf: Cover small conntrack opts error writes
  bpf: Guard conntrack opts error writes
  selftests/bpf: Cover half-slot cleanup of pointer spills
  bpf: Preserve pointer spill metadata during half-slot cleanup
  selftests/bpf: Test cgroup link replace with BPF_F_PREORDER
  bpf: Fix effective prog array index with BPF_F_PREORDER
  bpf: Fix BPF_PROG_ASSOC_STRUCT_OPS last field check
  bpf: zero-initialize the fib lookup flow struct
  bpftool: Fix vmlinux BTF leak in cgroup commands
  bpf: Add missing access_ok call to copy_user_syms
  bpf: Allow type tag BTF records to succeed other modifier records
  bpf: Emit verbose message when prog-specific btf_struct_access rejects a write
  bpf: Fix build_id caching in stack_map_get_build_id_offset()
  bpf: Fix partial copy of non-linear test_run output
  selftests/bpf: Cover stack nospec slot indexing
  bpf: Fix stack slot index in nospec checks
This commit is contained in:
Linus Torvalds
2026-06-25 14:09:26 -07:00
19 changed files with 535 additions and 239 deletions

View File

@@ -51,6 +51,9 @@ BTF_ID(func, bpf_lsm_key_getsecurity)
#ifdef CONFIG_AUDIT
BTF_ID(func, bpf_lsm_audit_rule_match)
#endif
#ifdef CONFIG_SECURITY_NETWORK_XFRM
BTF_ID(func, bpf_lsm_xfrm_decode_session)
#endif
BTF_ID(func, bpf_lsm_ismaclabel)
BTF_ID(func, bpf_lsm_file_alloc_security)
BTF_SET_END(bpf_lsm_disabled_hooks)

View File

@@ -28,6 +28,7 @@
#include <linux/string.h>
#include <linux/sysfs.h>
#include <linux/overflow.h>
#include <linux/bitops.h>
#include <net/netfilter/nf_bpf_link.h>
@@ -3472,12 +3473,69 @@ static int btf_find_struct(const struct btf *btf, const struct btf_type *t,
return BTF_FIELD_FOUND;
}
struct btf_type_tag_match {
const char *name;
u32 flag;
};
struct btf_type_tag_walk_ctx {
const struct btf_type *t; /* Input/Output */
u32 id; /* Output */
u32 res; /* Output */
};
static int btf_type_tag_walk(const struct btf *btf,
struct btf_type_tag_walk_ctx *ctx,
const struct btf_type_tag_match *matches,
u32 match_cnt)
{
const struct btf_type *t = ctx->t;
u32 res = 0;
const char *tag;
u32 id, i;
do {
id = t->type;
t = btf_type_by_id(btf, id);
if (!btf_type_is_modifier(t))
break;
if (!btf_type_is_type_tag(t) || btf_type_kflag(t))
continue;
tag = __btf_name_by_offset(btf, t->name_off);
for (i = 0; i < match_cnt; i++) {
if (strcmp(tag, matches[i].name))
continue;
res |= matches[i].flag;
break;
}
} while (true);
/* We only support a single tag. */
if (hweight32(res) > 1)
return -EINVAL;
ctx->t = t;
ctx->id = id;
ctx->res = res;
return 0;
}
static int btf_find_kptr(const struct btf *btf, const struct btf_type *t,
u32 off, int sz, struct btf_field_info *info, u32 field_mask)
{
enum btf_field_type type;
const char *tag_value;
bool is_type_tag;
static const struct btf_type_tag_match kptr_type_tags[] = {
{ "kptr_untrusted", BPF_KPTR_UNREF },
{ "kptr", BPF_KPTR_REF },
{ "percpu_kptr", BPF_KPTR_PERCPU },
{ "uptr", BPF_UPTR },
};
struct btf_type_tag_walk_ctx ctx;
enum btf_field_type type = 0;
int err;
u32 res_id;
/* Permit modifiers on the pointer itself */
@@ -3486,30 +3544,20 @@ static int btf_find_kptr(const struct btf *btf, const struct btf_type *t,
/* For PTR, sz is always == 8 */
if (!btf_type_is_ptr(t))
return BTF_FIELD_IGNORE;
t = btf_type_by_id(btf, t->type);
is_type_tag = btf_type_is_type_tag(t) && !btf_type_kflag(t);
if (!is_type_tag)
return BTF_FIELD_IGNORE;
/* Reject extra tags */
if (btf_type_is_type_tag(btf_type_by_id(btf, t->type)))
return -EINVAL;
tag_value = __btf_name_by_offset(btf, t->name_off);
if (!strcmp("kptr_untrusted", tag_value))
type = BPF_KPTR_UNREF;
else if (!strcmp("kptr", tag_value))
type = BPF_KPTR_REF;
else if (!strcmp("percpu_kptr", tag_value))
type = BPF_KPTR_PERCPU;
else if (!strcmp("uptr", tag_value))
type = BPF_UPTR;
else
return -EINVAL;
ctx.t = t;
err = btf_type_tag_walk(btf, &ctx, kptr_type_tags,
ARRAY_SIZE(kptr_type_tags));
if (err)
return err;
t = ctx.t;
res_id = ctx.id;
type = ctx.res;
if (!(type & field_mask))
return BTF_FIELD_IGNORE;
/* Get the base type */
t = btf_type_skip_modifiers(btf, t->type, &res_id);
/* Only pointer to struct is allowed */
if (!__btf_type_is_struct(t))
return -EINVAL;
@@ -5859,11 +5907,10 @@ struct btf_struct_meta *btf_find_struct_meta(const struct btf *btf, u32 btf_id)
return bsearch(&btf_id, tab->types, tab->cnt, sizeof(tab->types[0]), btf_id_cmp_func);
}
static int btf_check_type_tags(struct btf_verifier_env *env,
struct btf *btf, int start_id)
static int btf_check_modifier_chain_length(struct btf_verifier_env *env,
struct btf *btf, int start_id)
{
int i, n, good_id = start_id - 1;
bool in_tags;
n = btf_nr_types(btf);
for (i = start_id; i < n; i++) {
@@ -5879,20 +5926,12 @@ static int btf_check_type_tags(struct btf_verifier_env *env,
cond_resched();
in_tags = btf_type_is_type_tag(t);
while (btf_type_is_modifier(t)) {
if (!chain_limit--) {
btf_verifier_log(env, "Max chain length or cycle detected");
return -ELOOP;
}
if (btf_type_is_type_tag(t)) {
if (!in_tags) {
btf_verifier_log(env, "Type tags don't precede modifiers");
return -EINVAL;
}
} else if (in_tags) {
in_tags = false;
}
if (cur_id <= good_id)
break;
/* Move to next type */
@@ -5970,7 +6009,7 @@ static struct btf *btf_parse(const union bpf_attr *attr, bpfptr_t uattr,
if (err)
goto errout;
err = btf_check_type_tags(env, btf, 1);
err = btf_check_modifier_chain_length(env, btf, 1);
if (err)
goto errout;
@@ -6378,7 +6417,7 @@ static struct btf *btf_parse_base(struct btf_verifier_env *env, const char *name
if (err)
goto errout;
err = btf_check_type_tags(env, btf, 1);
err = btf_check_modifier_chain_length(env, btf, 1);
if (err)
goto errout;
@@ -6504,7 +6543,7 @@ static struct btf *btf_parse_module(const char *module_name, const void *data,
if (err)
goto errout;
err = btf_check_type_tags(env, btf, btf_nr_types(base_btf));
err = btf_check_modifier_chain_length(env, btf, btf_nr_types(base_btf));
if (err)
goto errout;
@@ -6810,14 +6849,18 @@ bool btf_ctx_access(int off, int size, enum bpf_access_type type,
const struct bpf_prog *prog,
struct bpf_insn_access_aux *info)
{
static const struct btf_type_tag_match ctx_type_tags[] = {
{ "user", MEM_USER },
{ "percpu", MEM_PERCPU },
};
const struct btf_type *t = prog->aux->attach_func_proto;
struct bpf_prog *tgt_prog = prog->aux->dst_prog;
struct btf *btf = bpf_prog_get_target_btf(prog);
const char *tname = prog->aux->attach_func_name;
struct bpf_verifier_log *log = info->log;
struct btf_type_tag_walk_ctx ctx;
const struct btf_param *args;
bool ptr_err_raw_tp = false;
const char *tag_value;
u32 nr_args, arg;
int i, ret;
@@ -7020,22 +7063,18 @@ bool btf_ctx_access(int off, int size, enum bpf_access_type type,
}
info->btf = btf;
info->btf_id = t->type;
t = btf_type_by_id(btf, t->type);
if (btf_type_is_type_tag(t) && !btf_type_kflag(t)) {
tag_value = __btf_name_by_offset(btf, t->name_off);
if (strcmp(tag_value, "user") == 0)
info->reg_type |= MEM_USER;
if (strcmp(tag_value, "percpu") == 0)
info->reg_type |= MEM_PERCPU;
ctx.t = t;
ret = btf_type_tag_walk(btf, &ctx, ctx_type_tags,
ARRAY_SIZE(ctx_type_tags));
if (ret) {
bpf_log(log, "func '%s' arg%d type %s has multiple type tags\n",
tname, arg, btf_type_str(t));
return false;
}
info->reg_type |= ctx.res;
info->btf_id = ctx.id;
t = ctx.t;
/* skip modifiers */
while (btf_type_is_modifier(t)) {
info->btf_id = t->type;
t = btf_type_by_id(btf, t->type);
}
if (!btf_type_is_struct(t)) {
bpf_log(log,
"func '%s' arg%d type %s is not a struct\n",
@@ -7074,7 +7113,7 @@ static int btf_struct_walk(struct bpf_verifier_log *log, const struct btf *btf,
u32 i, moff, mtrue_end, msize = 0, total_nelems = 0;
const struct btf_type *mtype, *elem_type = NULL;
const struct btf_member *member;
const char *tname, *mname, *tag_value;
const char *tname, *mname;
u32 vlen, elem_id, mid;
again:
@@ -7270,8 +7309,15 @@ static int btf_struct_walk(struct bpf_verifier_log *log, const struct btf *btf,
}
if (btf_type_is_ptr(mtype)) {
const struct btf_type *stype, *t;
static const struct btf_type_tag_match walk_type_tags[] = {
{ "user", MEM_USER },
{ "percpu", MEM_PERCPU },
{ "rcu", MEM_RCU },
};
enum bpf_type_flag tmp_flag = 0;
struct btf_type_tag_walk_ctx ctx = { .t = mtype };
const struct btf_type *stype;
int err;
u32 id;
if (msize != size || off != moff) {
@@ -7281,22 +7327,17 @@ static int btf_struct_walk(struct bpf_verifier_log *log, const struct btf *btf,
return -EACCES;
}
/* check type tag */
t = btf_type_by_id(btf, mtype->type);
if (btf_type_is_type_tag(t) && !btf_type_kflag(t)) {
tag_value = __btf_name_by_offset(btf, t->name_off);
/* check __user tag */
if (strcmp(tag_value, "user") == 0)
tmp_flag = MEM_USER;
/* check __percpu tag */
if (strcmp(tag_value, "percpu") == 0)
tmp_flag = MEM_PERCPU;
/* check __rcu tag */
if (strcmp(tag_value, "rcu") == 0)
tmp_flag = MEM_RCU;
err = btf_type_tag_walk(btf, &ctx, walk_type_tags,
ARRAY_SIZE(walk_type_tags));
if (err) {
bpf_log(log, "type '%s' has multiple type tags\n",
btf_type_str(mtype));
return err;
}
tmp_flag = ctx.res;
id = ctx.id;
stype = ctx.t;
stype = btf_type_skip_modifiers(btf, mtype->type, &id);
if (btf_type_is_struct(stype)) {
*next_btf_id = id;
*flag |= tmp_flag;
@@ -7867,7 +7908,12 @@ static int btf_scan_type_tags(struct bpf_verifier_env *env,
const struct btf *btf, u32 type_id,
u32 *tags)
{
static const struct btf_type_tag_match func_type_tags[] = {
{ "arena", ARG_TAG_ARENA },
};
struct btf_type_tag_walk_ctx ctx;
const struct btf_type *t;
int err;
/* Find the first pointer type in the chain. */
t = btf_type_skip_modifiers(btf, type_id, NULL);
@@ -7879,24 +7925,15 @@ static int btf_scan_type_tags(struct bpf_verifier_env *env,
if (!t || !btf_type_is_ptr(t))
return 0;
/* We got a pointer, get all associated type tags. */
for (t = btf_type_by_id(btf, t->type); t && btf_type_is_modifier(t);
t = btf_type_by_id(btf, t->type)) {
/* Skip non-type tag modifiers. */
if (!btf_type_is_type_tag(t))
continue;
const char *tag = __btf_name_by_offset(btf, t->name_off);
if (strcmp(tag, "arena") == 0) {
*tags |= ARG_TAG_ARENA;
} else {
bpf_log(&env->log, "function signature member has unsupported type tag '%s'\n",
tag);
return -EOPNOTSUPP;
}
ctx.t = t;
err = btf_type_tag_walk(btf, &ctx, func_type_tags,
ARRAY_SIZE(func_type_tags));
if (err) {
bpf_log(&env->log,
"function signature member has multiple type tags\n");
return err;
}
*tags |= ctx.res;
return 0;
}

View File

@@ -939,19 +939,65 @@ static int cgroup_bpf_attach(struct cgroup *cgrp,
return ret;
}
static int effective_prog_pos(struct cgroup *cgrp,
enum cgroup_bpf_attach_type atype,
struct bpf_prog_list *target_pl)
{
int cnt = 0, preorder_cnt = 0, fstart, bstart, init_bstart, pos = -1;
struct bpf_prog_list *pl;
struct cgroup *p = cgrp;
/* count effective programs to find where the preorder region ends */
do {
if (cnt == 0 || (p->bpf.flags[atype] & BPF_F_ALLOW_MULTI))
cnt += prog_list_length(&p->bpf.progs[atype], &preorder_cnt);
p = cgroup_parent(p);
} while (p);
/* replay compute_effective_progs() placement and record target's slot */
cnt = 0;
p = cgrp;
fstart = preorder_cnt;
bstart = preorder_cnt - 1;
do {
if (cnt > 0 && !(p->bpf.flags[atype] & BPF_F_ALLOW_MULTI))
continue;
init_bstart = bstart;
hlist_for_each_entry(pl, &p->bpf.progs[atype], node) {
if (!prog_list_prog(pl))
continue;
if (pl->flags & BPF_F_PREORDER) {
if (pl == target_pl)
pos = bstart;
bstart--;
} else {
if (pl == target_pl)
pos = fstart;
fstart++;
}
cnt++;
}
/* reverse pre-ordering progs at this cgroup level */
if (pos >= bstart + 1 && pos <= init_bstart)
pos = bstart + 1 + init_bstart - pos;
} while ((p = cgroup_parent(p)));
return pos;
}
/* Swap updated BPF program for given link in effective program arrays across
* all descendant cgroups. This function is guaranteed to succeed.
*/
static void replace_effective_prog(struct cgroup *cgrp,
enum cgroup_bpf_attach_type atype,
struct bpf_cgroup_link *link)
struct bpf_prog_list *pl)
{
struct bpf_prog_array_item *item;
struct cgroup_subsys_state *css;
struct bpf_prog_array *progs;
struct bpf_prog_list *pl;
struct hlist_head *head;
struct cgroup *cg;
int pos;
css_for_each_descendant_pre(css, &cgrp->self) {
@@ -960,27 +1006,15 @@ static void replace_effective_prog(struct cgroup *cgrp,
if (percpu_ref_is_zero(&desc->bpf.refcnt))
continue;
/* find position of link in effective progs array */
for (pos = 0, cg = desc; cg; cg = cgroup_parent(cg)) {
if (pos && !(cg->bpf.flags[atype] & BPF_F_ALLOW_MULTI))
continue;
pos = effective_prog_pos(desc, atype, pl);
if (WARN_ON_ONCE(pos < 0))
continue;
head = &cg->bpf.progs[atype];
hlist_for_each_entry(pl, head, node) {
if (!prog_list_prog(pl))
continue;
if (pl->link == link)
goto found;
pos++;
}
}
found:
BUG_ON(!cg);
progs = rcu_dereference_protected(
desc->bpf.effective[atype],
lockdep_is_held(&cgroup_mutex));
item = &progs->items[pos];
WRITE_ONCE(item->prog, link->link.prog);
WRITE_ONCE(item->prog, pl->link->link.prog);
}
}
@@ -1024,7 +1058,7 @@ static int __cgroup_bpf_replace(struct cgroup *cgrp,
cgrp->bpf.revisions[atype] += 1;
old_prog = xchg(&link->link.prog, new_prog);
replace_effective_prog(cgrp, atype, link);
replace_effective_prog(cgrp, atype, pl);
bpf_prog_put(old_prog);
return 0;
}
@@ -1091,19 +1125,14 @@ static struct bpf_prog_list *find_detach_entry(struct hlist_head *progs,
* recomputing the array in place.
*
* @cgrp: The cgroup which descendants to travers
* @prog: A program to detach or NULL
* @link: A link to detach or NULL
* @pl: The prog_list entry being detached
* @atype: Type of detach operation
*/
static void purge_effective_progs(struct cgroup *cgrp, struct bpf_prog *prog,
struct bpf_cgroup_link *link,
static void purge_effective_progs(struct cgroup *cgrp, struct bpf_prog_list *pl,
enum cgroup_bpf_attach_type atype)
{
struct cgroup_subsys_state *css;
struct bpf_prog_array *progs;
struct bpf_prog_list *pl;
struct hlist_head *head;
struct cgroup *cg;
int pos;
/* recompute effective prog array in place */
@@ -1113,24 +1142,11 @@ static void purge_effective_progs(struct cgroup *cgrp, struct bpf_prog *prog,
if (percpu_ref_is_zero(&desc->bpf.refcnt))
continue;
/* find position of link or prog in effective progs array */
for (pos = 0, cg = desc; cg; cg = cgroup_parent(cg)) {
if (pos && !(cg->bpf.flags[atype] & BPF_F_ALLOW_MULTI))
continue;
head = &cg->bpf.progs[atype];
hlist_for_each_entry(pl, head, node) {
if (!prog_list_prog(pl))
continue;
if (pl->prog == prog && pl->link == link)
goto found;
pos++;
}
}
pos = effective_prog_pos(desc, atype, pl);
/* no link or prog match, skip the cgroup of this layer */
continue;
found:
if (pos < 0)
continue;
progs = rcu_dereference_protected(
desc->bpf.effective[atype],
lockdep_is_held(&cgroup_mutex));
@@ -1196,7 +1212,7 @@ static int __cgroup_bpf_detach(struct cgroup *cgrp, struct bpf_prog *prog,
/* if update effective array failed replace the prog with a dummy prog*/
pl->prog = old_prog;
pl->link = link;
purge_effective_progs(cgrp, old_prog, link, atype);
purge_effective_progs(cgrp, pl, atype);
}
/* now can actually delete it from this cgroup list */

View File

@@ -175,6 +175,95 @@ static inline void stack_map_build_id_set_valid(struct bpf_stack_build_id *id,
memcpy(id->build_id, build_id, BUILD_ID_SIZE_MAX);
}
/*
* A cached VMA lookup result. The range [vm_start, vm_end) is always set.
* vm_pgoff, file, build_id are set only when the build ID was resolved.
* Zero vm_end marks the slot empty. build_id aliases the id_offs[] entry.
*/
struct stack_map_cached_vma {
unsigned long vm_start;
unsigned long vm_end;
unsigned long vm_pgoff;
struct file *file; /* pinned in the sleepable path; NULL otherwise */
const unsigned char *build_id;
};
/*
* Per stack_map_get_build_id_offset() call cache of the last VMA with a build ID
* resolved and the last VMA with no usable build ID. Adjacent stack frames tend
* to land in the same VMA or the same backing file, so caching the last result
* of each kind lets us skip unnecessary VMA lookups and build ID parse calls.
* Keeping the two slots independent means a build-ID-less VMA doesn't evict the
* last resolved build ID.
*/
struct stack_map_build_id_cache {
struct stack_map_cached_vma resolved;
struct stack_map_cached_vma unresolved;
};
/*
* Fill @id from a cached range covering @ip. On a hit this writes @id (resolved
* range -> build ID + offset, unresolved range -> raw ip) and returns 0; on a
* miss it leaves @id untouched and returns -ENOENT.
*/
static int stack_map_build_id_set_from_cache(struct stack_map_build_id_cache *cache,
struct bpf_stack_build_id *id, u64 ip)
{
unsigned long vm_start, vm_end, vm_pgoff;
u64 offset;
vm_start = cache->resolved.vm_start;
vm_end = cache->resolved.vm_end;
if (vm_end && ip >= vm_start && ip < vm_end) {
vm_pgoff = cache->resolved.vm_pgoff;
offset = stack_map_build_id_offset(vm_pgoff, vm_start, ip);
stack_map_build_id_set_valid(id, offset, cache->resolved.build_id);
return 0;
}
vm_start = cache->unresolved.vm_start;
vm_end = cache->unresolved.vm_end;
if (vm_end && ip >= vm_start && ip < vm_end) {
stack_map_build_id_set_ip(id);
return 0;
}
return -ENOENT;
}
/*
* Record @vma's build ID as the last resolved one. @file is the pinned backing
* file in the sleepable path (released when evicted), or NULL otherwise.
*/
static void stack_map_build_id_cache_set_resolved(struct stack_map_build_id_cache *cache,
struct file *file,
const unsigned char *build_id,
unsigned long vm_start,
unsigned long vm_end,
unsigned long vm_pgoff)
{
if (cache->resolved.file)
fput(cache->resolved.file);
cache->resolved = (struct stack_map_cached_vma){
.vm_start = vm_start,
.vm_end = vm_end,
.vm_pgoff = vm_pgoff,
.file = file,
.build_id = build_id,
};
}
/* Record [vm_start, vm_end) as a range with no usable build ID. */
static void stack_map_build_id_cache_set_unresolved(struct stack_map_build_id_cache *cache,
unsigned long vm_start,
unsigned long vm_end)
{
cache->unresolved = (struct stack_map_cached_vma){
.vm_start = vm_start,
.vm_end = vm_end,
};
}
struct stack_map_vma_lock {
struct vm_area_struct *vma;
struct mm_struct *mm;
@@ -244,15 +333,9 @@ static void stack_map_unlock_vma(struct stack_map_vma_lock *lock)
static void stack_map_get_build_id_offset_sleepable(struct bpf_stack_build_id *id_offs,
u32 trace_nr)
{
struct mm_struct *mm = current->mm;
struct stack_map_vma_lock lock = { .mm = mm };
struct {
struct file *file;
const unsigned char *build_id;
unsigned long vm_start;
unsigned long vm_end;
unsigned long vm_pgoff;
} cache = {};
struct stack_map_vma_lock lock = { .mm = current->mm };
struct stack_map_build_id_cache cache = {};
struct stack_map_cached_vma *res = &cache.resolved;
unsigned long vm_pgoff, vm_start, vm_end;
struct vm_area_struct *vma;
struct file *file;
@@ -262,44 +345,39 @@ static void stack_map_get_build_id_offset_sleepable(struct bpf_stack_build_id *i
for (u32 i = 0; i < trace_nr; i++) {
ip = READ_ONCE(id_offs[i].ip);
/*
* Range cache fast path: if ip falls within the previously
* resolved VMA range, reuse the cache build_id without
* re-acquiring the VMA lock.
*/
if (cache.build_id && ip >= cache.vm_start && ip < cache.vm_end) {
offset = stack_map_build_id_offset(cache.vm_pgoff, cache.vm_start, ip);
stack_map_build_id_set_valid(&id_offs[i], offset, cache.build_id);
if (!stack_map_build_id_set_from_cache(&cache, &id_offs[i], ip))
continue;
}
vma = stack_map_lock_vma(&lock, ip);
if (!vma) {
stack_map_build_id_set_ip(&id_offs[i]);
continue;
}
vm_pgoff = vma->vm_pgoff;
vm_start = vma->vm_start;
vm_end = vma->vm_end;
if (vma_is_anonymous(vma) || !vma->vm_file) {
stack_map_build_id_set_ip(&id_offs[i]);
stack_map_unlock_vma(&lock);
stack_map_build_id_set_ip(&id_offs[i]);
stack_map_build_id_cache_set_unresolved(&cache, vm_start, vm_end);
continue;
}
file = vma->vm_file;
vm_pgoff = vma->vm_pgoff;
vm_start = vma->vm_start;
vm_end = vma->vm_end;
offset = stack_map_build_id_offset(vm_pgoff, vm_start, ip);
/*
* Same backing file as previous (e.g. different VMAs
* of the same ELF binary). Reuse the cache build_id.
* Same backing file as the last resolved VMA (another mapping
* of the same ELF binary): reuse its build_id without re-parsing.
*/
if (file == cache.file) {
if (file == res->file) {
stack_map_unlock_vma(&lock);
stack_map_build_id_set_valid(&id_offs[i], offset, cache.build_id);
cache.vm_start = vm_start;
cache.vm_end = vm_end;
cache.vm_pgoff = vm_pgoff;
stack_map_build_id_set_valid(&id_offs[i], offset, res->build_id);
res->vm_start = vm_start;
res->vm_end = vm_end;
res->vm_pgoff = vm_pgoff;
continue;
}
@@ -310,21 +388,17 @@ static void stack_map_get_build_id_offset_sleepable(struct bpf_stack_build_id *i
if (build_id_parse_file(file, id_offs[i].build_id, NULL)) {
stack_map_build_id_set_ip(&id_offs[i]);
fput(file);
stack_map_build_id_cache_set_unresolved(&cache, vm_start, vm_end);
continue;
}
stack_map_build_id_set_valid(&id_offs[i], offset, id_offs[i].build_id);
if (cache.file)
fput(cache.file);
cache.file = file;
cache.build_id = id_offs[i].build_id;
cache.vm_start = vm_start;
cache.vm_end = vm_end;
cache.vm_pgoff = vm_pgoff;
stack_map_build_id_cache_set_resolved(&cache, file, id_offs[i].build_id,
vm_start, vm_end, vm_pgoff);
}
if (cache.file)
fput(cache.file);
if (res->file)
fput(res->file);
}
/*
@@ -343,8 +417,8 @@ static void stack_map_get_build_id_offset(struct bpf_stack_build_id *id_offs,
struct mmap_unlock_irq_work *work = NULL;
bool irq_work_busy = bpf_mmap_unlock_get_irq_work(&work);
bool has_user_ctx = user && current && current->mm;
struct vm_area_struct *vma, *prev_vma = NULL;
const unsigned char *prev_build_id = NULL;
struct stack_map_build_id_cache cache = {};
struct vm_area_struct *vma;
int i;
if (may_fault && has_user_ctx) {
@@ -365,27 +439,30 @@ static void stack_map_get_build_id_offset(struct bpf_stack_build_id *id_offs,
for (i = 0; i < trace_nr; i++) {
u64 ip = READ_ONCE(id_offs[i].ip);
u64 offset;
if (prev_build_id && range_in_vma(prev_vma, ip, ip)) {
vma = prev_vma;
offset = stack_map_build_id_offset(vma->vm_pgoff, vma->vm_start, ip);
stack_map_build_id_set_valid(&id_offs[i], offset, prev_build_id);
if (!stack_map_build_id_set_from_cache(&cache, &id_offs[i], ip))
continue;
}
vma = find_vma(current->mm, ip);
if (!vma || vma_is_anonymous(vma) ||
fetch_build_id(vma, id_offs[i].build_id, may_fault)) {
/* per entry fall back to ips */
/* per entry fall back to ips; cache build-ID-less range */
stack_map_build_id_set_ip(&id_offs[i]);
prev_vma = vma;
prev_build_id = NULL;
if (vma)
stack_map_build_id_cache_set_unresolved(&cache,
vma->vm_start, vma->vm_end);
continue;
}
offset = stack_map_build_id_offset(vma->vm_pgoff, vma->vm_start, ip);
stack_map_build_id_set_valid(&id_offs[i], offset, id_offs[i].build_id);
prev_vma = vma;
prev_build_id = id_offs[i].build_id;
/*
* mmap_lock is held for the whole loop, so the cached VMA
* fields stay valid; no file pinning is needed here.
*/
stack_map_build_id_set_valid(&id_offs[i],
stack_map_build_id_offset(vma->vm_pgoff, vma->vm_start, ip),
id_offs[i].build_id);
stack_map_build_id_cache_set_resolved(&cache, NULL, id_offs[i].build_id,
vma->vm_start, vma->vm_end,
vma->vm_pgoff);
}
bpf_mmap_unlock_mm(work, current->mm);
}

View File

@@ -436,12 +436,10 @@ static void __clean_func_state(struct bpf_verifier_env *env,
continue;
/*
* Only destroy spilled_ptr when hi half is dead.
* If hi half is still live with STACK_SPILL, the
* spilled_ptr metadata is needed for correct state
* comparison in stacksafe().
* is_spilled_reg() is using slot_type[7], but
* is_spilled_scalar_after() check either slot_type[0] or [4]
* Only scalar spills can be degraded to raw stack bytes
* when their high half is dead. Pointer spills need the
* saved spilled_ptr metadata so partial fills keep
* rejecting as non-scalar register fills.
*/
if (!hi_live) {
struct bpf_reg_state *spill = &st->stack[i].spilled_ptr;
@@ -449,6 +447,9 @@ static void __clean_func_state(struct bpf_verifier_env *env,
if (lo_live && stype == STACK_SPILL) {
u8 val = STACK_MISC;
if (spill->type != SCALAR_VALUE)
continue;
/*
* 8 byte spill of scalar 0 where half slot is dead
* should become STACK_ZERO in lo 4 bytes.

View File

@@ -6308,7 +6308,7 @@ static int prog_stream_read(union bpf_attr *attr)
return ret;
}
#define BPF_PROG_ASSOC_STRUCT_OPS_LAST_FIELD prog_assoc_struct_ops.prog_fd
#define BPF_PROG_ASSOC_STRUCT_OPS_LAST_FIELD prog_assoc_struct_ops.flags
static int prog_assoc_struct_ops(union bpf_attr *attr)
{

View File

@@ -3479,7 +3479,8 @@ static int check_stack_write_fixed_off(struct bpf_verifier_env *env,
bool sanitize = reg && is_spillable_regtype(reg->type);
for (i = 0; i < size; i++) {
u8 type = state->stack[spi].slot_type[i];
u8 type = state->stack[spi].slot_type[(slot - i) %
BPF_REG_SIZE];
if (type != STACK_MISC && type != STACK_ZERO) {
sanitize = true;
@@ -5786,6 +5787,10 @@ static int check_ptr_to_btf_access(struct bpf_verifier_env *env,
return -EFAULT;
}
ret = env->ops->btf_struct_access(&env->log, reg, off, size);
if (ret < 0)
verbose(env,
"%s cannot write into ptr_%s at off=%d size=%d\n",
reg_arg_name(env, argno), tname, off, size);
} else {
/* Writes are permitted with default btf_struct_access for
* program allocated objects (which always have id > 0),
@@ -6196,6 +6201,7 @@ static int check_mem_access(struct bpf_verifier_env *env, int insn_idx, struct b
*/
if (info.reg_type == SCALAR_VALUE) {
if (info.is_retval && get_func_retval_range(env->prog, &range)) {
mark_reg_unknown(env, regs, value_regno);
err = __mark_reg_s32_range(env, regs, value_regno,
range.minval, range.maxval);
if (err)

View File

@@ -2376,9 +2376,12 @@ static int copy_user_syms(struct user_syms *us, unsigned long __user *usyms, u32
int err = -ENOMEM;
unsigned int i;
if (!access_ok(usyms, cnt * sizeof(*usyms)))
return -EFAULT;
syms = kvmalloc_array(cnt, sizeof(*syms), GFP_KERNEL);
if (!syms)
goto error;
return -ENOMEM;
buf = kvmalloc_array(cnt, KSYM_NAME_LEN, GFP_KERNEL);
if (!buf)
@@ -2403,10 +2406,8 @@ static int copy_user_syms(struct user_syms *us, unsigned long __user *usyms, u32
return 0;
error:
if (err) {
kvfree(syms);
kvfree(buf);
}
kvfree(syms);
kvfree(buf);
return err;
}

View File

@@ -452,12 +452,8 @@ static int bpf_test_finish(const union bpf_attr *kattr,
}
if (data_out) {
int len = sinfo ? copy_size - frag_size : copy_size;
if (len < 0) {
err = -ENOSPC;
goto out;
}
u32 head_len = size - frag_size;
u32 len = min(copy_size, head_len);
if (copy_to_user(data_out, data, len))
goto out;

View File

@@ -6221,7 +6221,7 @@ static int bpf_ipv4_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
struct in_device *in_dev;
struct net_device *dev;
struct fib_result res;
struct flowi4 fl4;
struct flowi4 fl4 = {};
u32 mtu = 0;
int err;
@@ -6361,7 +6361,7 @@ static int bpf_ipv6_fib_lookup(struct net *net, struct bpf_fib_lookup *params,
struct neighbour *neigh;
struct net_device *dev;
struct inet6_dev *idev;
struct flowi6 fl6;
struct flowi6 fl6 = {};
int strict = 0;
int oif, err;
u32 mtu = 0;

View File

@@ -65,6 +65,15 @@ enum {
NF_BPF_CT_OPTS_SZ = 16,
};
static void *bpf_ct_opts_result(struct bpf_ct_opts *opts, u32 opts__sz, void *ret)
{
if (!IS_ERR(ret))
return ret;
if (opts__sz >= offsetofend(struct bpf_ct_opts, error))
opts->error = PTR_ERR(ret);
return NULL;
}
static int bpf_nf_ct_tuple_parse(struct bpf_sock_tuple *bpf_tuple,
u32 tuple_len, u8 protonum, u8 dir,
struct nf_conntrack_tuple *tuple)
@@ -297,12 +306,7 @@ bpf_xdp_ct_alloc(struct xdp_md *xdp_ctx, struct bpf_sock_tuple *bpf_tuple,
nfct = __bpf_nf_ct_alloc_entry(dev_net(ctx->rxq->dev), bpf_tuple, tuple__sz,
opts, opts__sz, 10);
if (IS_ERR(nfct)) {
opts->error = PTR_ERR(nfct);
return NULL;
}
return (struct nf_conn___init *)nfct;
return (struct nf_conn___init *)bpf_ct_opts_result(opts, opts__sz, nfct);
}
/* bpf_xdp_ct_lookup - Lookup CT entry for the given tuple, and acquire a
@@ -331,11 +335,7 @@ bpf_xdp_ct_lookup(struct xdp_md *xdp_ctx, struct bpf_sock_tuple *bpf_tuple,
caller_net = dev_net(ctx->rxq->dev);
nfct = __bpf_nf_ct_lookup(caller_net, bpf_tuple, tuple__sz, opts, opts__sz);
if (IS_ERR(nfct)) {
opts->error = PTR_ERR(nfct);
return NULL;
}
return nfct;
return bpf_ct_opts_result(opts, opts__sz, nfct);
}
/* bpf_skb_ct_alloc - Allocate a new CT entry
@@ -363,12 +363,7 @@ bpf_skb_ct_alloc(struct __sk_buff *skb_ctx, struct bpf_sock_tuple *bpf_tuple,
net = skb->dev ? dev_net(skb->dev) : sock_net(skb->sk);
nfct = __bpf_nf_ct_alloc_entry(net, bpf_tuple, tuple__sz, opts, opts__sz, 10);
if (IS_ERR(nfct)) {
opts->error = PTR_ERR(nfct);
return NULL;
}
return (struct nf_conn___init *)nfct;
return (struct nf_conn___init *)bpf_ct_opts_result(opts, opts__sz, nfct);
}
/* bpf_skb_ct_lookup - Lookup CT entry for the given tuple, and acquire a
@@ -397,11 +392,7 @@ bpf_skb_ct_lookup(struct __sk_buff *skb_ctx, struct bpf_sock_tuple *bpf_tuple,
caller_net = skb->dev ? dev_net(skb->dev) : sock_net(skb->sk);
nfct = __bpf_nf_ct_lookup(caller_net, bpf_tuple, tuple__sz, opts, opts__sz);
if (IS_ERR(nfct)) {
opts->error = PTR_ERR(nfct);
return NULL;
}
return nfct;
return bpf_ct_opts_result(opts, opts__sz, nfct);
}
/* bpf_ct_insert_entry - Add the provided entry into a CT map

View File

@@ -78,6 +78,13 @@ static unsigned int query_flags;
static struct btf *btf_vmlinux;
static __u32 btf_vmlinux_id;
static void free_btf_vmlinux(void)
{
btf__free(btf_vmlinux);
btf_vmlinux = NULL;
btf_vmlinux_id = 0;
}
static enum bpf_attach_type parse_attach_type(const char *str)
{
const char *attach_type_str;
@@ -388,6 +395,8 @@ static int do_show(int argc, char **argv)
if (json_output)
jsonw_end_array(json_wtr);
free_btf_vmlinux();
exit_cgroup:
close(cgroup_fd);
exit:
@@ -437,7 +446,9 @@ static int do_show_tree_fn(const char *fpath, const struct stat *sb,
printf("%s\n", fpath);
}
btf_vmlinux = libbpf_find_kernel_btf();
if (!btf_vmlinux)
btf_vmlinux = libbpf_find_kernel_btf();
for (i = 0; i < ARRAY_SIZE(cgroup_attach_types); i++)
show_bpf_progs(cgroup_fd, cgroup_attach_types[i], ftw->level);
@@ -540,6 +551,7 @@ static int do_show_tree(int argc, char **argv)
if (json_output)
jsonw_end_array(json_wtr);
free_btf_vmlinux();
free(cgroup_alloced);
return ret;

View File

@@ -5,6 +5,8 @@
#include "test_bpf_nf.skel.h"
#include "test_bpf_nf_fail.skel.h"
#define CT_OPTS_ERROR_GUARD 0x12345678
static char log_buf[1024 * 1024];
struct {
@@ -119,6 +121,10 @@ static void test_bpf_nf_ct(int mode)
ASSERT_EQ(skel->bss->test_einval_reserved_new, -EINVAL, "Test EINVAL for reserved in new struct not set to 0");
ASSERT_EQ(skel->bss->test_einval_netns_id, -EINVAL, "Test EINVAL for netns_id < -1");
ASSERT_EQ(skel->bss->test_einval_len_opts, -EINVAL, "Test EINVAL for len__opts != NF_BPF_CT_OPTS_SZ");
ASSERT_EQ(skel->bss->test_einval_len_opts_small_lookup, CT_OPTS_ERROR_GUARD,
"Test no error write for lookup opts__sz before error field");
ASSERT_EQ(skel->bss->test_einval_len_opts_small_alloc, CT_OPTS_ERROR_GUARD,
"Test no error write for alloc opts__sz before error field");
ASSERT_EQ(skel->bss->test_eproto_l4proto, -EPROTO, "Test EPROTO for l4proto != TCP or UDP");
ASSERT_EQ(skel->bss->test_enonet_netns_id, -ENONET, "Test ENONET for bad but valid netns_id");
ASSERT_EQ(skel->bss->test_enoent_lookup, -ENOENT, "Test ENOENT for failed lookup");

View File

@@ -4121,8 +4121,6 @@ static struct btf_raw_test raw_tests[] = {
.key_type_id = 1,
.value_type_id = 1,
.max_entries = 1,
.btf_load_err = true,
.err_str = "Type tags don't precede modifiers",
},
{
.descr = "type_tag test #3, type tag order",
@@ -4141,8 +4139,6 @@ static struct btf_raw_test raw_tests[] = {
.key_type_id = 1,
.value_type_id = 1,
.max_entries = 1,
.btf_load_err = true,
.err_str = "Type tags don't precede modifiers",
},
{
.descr = "type_tag test #4, type tag order",
@@ -4161,8 +4157,6 @@ static struct btf_raw_test raw_tests[] = {
.key_type_id = 1,
.value_type_id = 1,
.max_entries = 1,
.btf_load_err = true,
.err_str = "Type tags don't precede modifiers",
},
{
.descr = "type_tag test #5, type tag order",
@@ -4198,11 +4192,9 @@ static struct btf_raw_test raw_tests[] = {
.map_name = "tag_type_check_btf",
.key_size = sizeof(int),
.value_size = 4,
.key_type_id = 1,
.value_type_id = 1,
.key_type_id = 4,
.value_type_id = 4,
.max_entries = 1,
.btf_load_err = true,
.err_str = "Type tags don't precede modifiers",
},
{
.descr = "type_tag test #7, tag with kflag",

View File

@@ -102,6 +102,82 @@ static int run_getsockopt_test(int cg_parent, int cg_child, int sock_fd, bool al
return err;
}
/*
* Replacing a link's program (bpf_link_update) must target the correct slot in
* the effective array even when a BPF_F_PREORDER program is attached to the
* same cgroup. All programs here are attached to a single cgroup; "parent" is
* reused only as a third distinct program.
*
* Attach child(1) normally and child_2(2) with BPF_F_PREORDER, so the effective
* order is [2, 1]. Then replace child(1)'s program with parent(3): only the
* non-preorder slot changes, giving [2, 3].
*/
static int run_link_replace_test(int cgroup_fd, int sock_fd)
{
LIBBPF_OPTS(bpf_link_create_opts, create_opts);
int err = 0, normal_link = -1, preorder_link = -1;
struct cgroup_preorder *skel = NULL;
enum bpf_attach_type atype;
__u8 *result, buf = 0x00;
socklen_t optlen = 1;
skel = cgroup_preorder__open_and_load();
if (!ASSERT_OK_PTR(skel, "cgroup_preorder__open_and_load"))
return -1;
err = setsockopt(sock_fd, SOL_IP, IP_TOS, &buf, 1);
if (!ASSERT_OK(err, "setsockopt"))
goto close_skel;
atype = bpf_program__expected_attach_type(skel->progs.child);
create_opts.flags = 0;
normal_link = bpf_link_create(bpf_program__fd(skel->progs.child),
cgroup_fd, atype, &create_opts);
if (!ASSERT_GE(normal_link, 0, "create_normal_link")) {
err = normal_link;
goto close_skel;
}
create_opts.flags = BPF_F_PREORDER;
preorder_link = bpf_link_create(bpf_program__fd(skel->progs.child_2),
cgroup_fd, atype, &create_opts);
if (!ASSERT_GE(preorder_link, 0, "create_preorder_link")) {
err = preorder_link;
goto close_links;
}
result = skel->bss->result;
skel->bss->idx = 0;
memset(result, 0, 4);
err = getsockopt(sock_fd, SOL_IP, IP_TOS, &buf, &optlen);
if (!ASSERT_OK(err, "getsockopt-before"))
goto close_links;
ASSERT_TRUE(result[0] == 2 && result[1] == 1, "order before update");
/* Replace the normal link's program child(1) -> parent(3). */
err = bpf_link_update(normal_link, bpf_program__fd(skel->progs.parent), NULL);
if (!ASSERT_OK(err, "bpf_link_update"))
goto close_links;
skel->bss->idx = 0;
memset(result, 0, 4);
err = getsockopt(sock_fd, SOL_IP, IP_TOS, &buf, &optlen);
if (!ASSERT_OK(err, "getsockopt-after"))
goto close_links;
ASSERT_TRUE(result[0] == 2 && result[1] == 3, "order after update");
close_links:
if (preorder_link >= 0)
close(preorder_link);
close(normal_link);
close_skel:
cgroup_preorder__destroy(skel);
return err;
}
void test_cgroup_preorder(void)
{
int cg_parent = -1, cg_child = -1, sock_fd = -1;
@@ -120,6 +196,7 @@ void test_cgroup_preorder(void)
ASSERT_OK(run_getsockopt_test(cg_parent, cg_child, sock_fd, false), "getsockopt_test_1");
ASSERT_OK(run_getsockopt_test(cg_parent, cg_child, sock_fd, true), "getsockopt_test_2");
ASSERT_OK(run_link_replace_test(cg_child, sock_fd), "link_replace_test");
out:
close(sock_fd);

View File

@@ -10,6 +10,8 @@
#define EINVAL 22
#define ENOENT 2
#define CT_OPTS_ERROR_GUARD 0x12345678
#define NF_CT_ZONE_DIR_ORIG (1 << IP_CT_DIR_ORIGINAL)
#define NF_CT_ZONE_DIR_REPL (1 << IP_CT_DIR_REPLY)
@@ -19,6 +21,8 @@ int test_einval_reserved = 0;
int test_einval_reserved_new = 0;
int test_einval_netns_id = 0;
int test_einval_len_opts = 0;
int test_einval_len_opts_small_lookup = 0;
int test_einval_len_opts_small_alloc = 0;
int test_eproto_l4proto = 0;
int test_enonet_netns_id = 0;
int test_enoent_lookup = 0;
@@ -124,6 +128,28 @@ nf_ct_test(struct nf_conn *(*lookup_fn)(void *, struct bpf_sock_tuple *, u32,
else
test_einval_len_opts = opts_def.error;
opts_def.error = CT_OPTS_ERROR_GUARD;
ct = lookup_fn(ctx, &bpf_tuple, sizeof(bpf_tuple.ipv4), &opts_def,
sizeof(opts_def.netns_id));
if (ct) {
bpf_ct_release(ct);
test_einval_len_opts_small_lookup = -EINVAL;
} else {
test_einval_len_opts_small_lookup = opts_def.error;
}
opts_def.error = CT_OPTS_ERROR_GUARD;
ct = alloc_fn(ctx, &bpf_tuple, sizeof(bpf_tuple.ipv4), &opts_def,
sizeof(opts_def.netns_id));
if (ct) {
ct = bpf_ct_insert_entry(ct);
if (ct)
bpf_ct_release(ct);
test_einval_len_opts_small_alloc = -EINVAL;
} else {
test_einval_len_opts_small_alloc = opts_def.error;
}
opts_def.l4proto = IPPROTO_ICMP;
ct = lookup_fn(ctx, &bpf_tuple, sizeof(bpf_tuple.ipv4), &opts_def,
sizeof(opts_def));

View File

@@ -197,4 +197,19 @@ int BPF_PROG(sleepable_lsm_cgroup)
return 0;
}
SEC("lsm/file_mprotect")
__description("lsm retval load must reset stale register bounds")
__failure __msg("div by zero")
__naked int retval_load_resets_bounds(void *ctx)
{
asm volatile (
"r6 = 0;"
"r6 = *(u64 *)(r1 + 24);"
"if r6 == 0 goto +1;"
"r6 /= 0;"
"r0 = 0;"
"exit;"
::: __clobber_all);
}
char _license[] SEC("license") = "GPL";

View File

@@ -1359,4 +1359,22 @@ __naked void var_off_write_over_scalar_spill(void)
: __clobber_all);
}
SEC("socket")
__description("partial fill from cleaned pointer spill")
__failure
__log_level(2)
__msg("1: (05) goto pc+0")
__msg("2: (61) r0 = *(u32 *)(r10 -4)")
__msg("invalid size of register fill")
__flag(BPF_F_TEST_STATE_FREQ)
__naked void partial_fill_from_cleaned_pointer_spill(void)
{
/* Spill R1(ctx), then force a checkpoint and half-slot cleanup. */
asm volatile ("*(u64 *)(r10 - 8) = r1;"
"goto +0;"
"r0 = *(u32 *)(r10 - 4);"
"exit;"
::: __clobber_all);
}
char _license[] SEC("license") = "GPL";

View File

@@ -976,4 +976,26 @@ l0_%=: exit; \
: __clobber_all);
}
SEC("socket")
__description("unpriv: Spectre v4 stack write slot index")
__success __success_unpriv
__retval(0)
#ifdef SPEC_V4
__xlated_unpriv("r0 = 0")
__xlated_unpriv("*(u32 *)(r10 -4) = r0")
__xlated_unpriv("nospec")
__xlated_unpriv("*(u32 *)(r10 -8) = r0")
__xlated_unpriv("nospec")
__xlated_unpriv("exit")
#endif
__naked void stack_write_nospec_slot_index(void)
{
asm volatile (" \
r0 = 0; \
*(u32 *)(r10 - 4) = r0; \
*(u32 *)(r10 - 8) = r0; \
exit; \
" ::: __clobber_all);
}
char _license[] SEC("license") = "GPL";