mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-08-27 16:28:58 -04:00
GCC emits BTF line records with a file name and line number, but leaves the
source line string empty. bpf_diag_source() currently treats that empty string
as if the complete line record were unavailable, so diagnostics fall back to
an instruction number and discard the function, file, and line attribution.
Print the available source location before deciding whether source context can
be rendered. When source text is absent, omit only the source context and retain
the diagnostic annotation and instruction context.
Fixes: b9c5d822f6 ("bpf: Add source and instruction diagnostic context")
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
Acked-by: Eduard Zingerman <eddyz87@gmail.com>
Link: https://lore.kernel.org/bpf/20260816015746.2632990-12-memxor@gmail.com
2360 lines
64 KiB
C
2360 lines
64 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
// Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
|
|
|
|
#include <linux/bpf.h>
|
|
#include <linux/bpf_verifier.h>
|
|
#include <linux/btf.h>
|
|
#include <linux/ctype.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/list.h>
|
|
#include <linux/seq_buf.h>
|
|
#include <linux/overflow.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/stdarg.h>
|
|
#include <linux/string.h>
|
|
|
|
#include "disasm.h"
|
|
#include "diagnostics.h"
|
|
|
|
#define REGISTER_TYPE_SAFETY "Register Type Safety"
|
|
#define MEMORY_SAFETY "Memory Safety"
|
|
#define RESOURCE_LIFETIME_SAFETY "Resource Lifetime Safety"
|
|
#define CALL_TYPE_SAFETY "Call Type Safety"
|
|
#define EXECUTION_CONTEXT_SAFETY "Execution Context Safety"
|
|
#define PROGRAM_STRUCTURE "Program Structure"
|
|
#define POLICY "Policy"
|
|
|
|
#define BPF_DIAG_TEXT_WIDTH 100
|
|
#define BPF_DIAG_TEXT_INDENT " "
|
|
#define BPF_DIAG_CONTEXT 2
|
|
#define BPF_DIAG_CONTEXT_CNT (1 + BPF_DIAG_CONTEXT * 2)
|
|
#define BPF_DIAG_HISTORY_RENDER_MAX 64
|
|
#define BPF_DIAG_SOURCE_LANE_WIDTH 88
|
|
#define BPF_DIAG_TAB_WIDTH 8
|
|
#define BPF_DIAG_FMT_CHUNK_SIZE (PAGE_SIZE - sizeof(struct diag_fmt_chunk))
|
|
#define BPF_DIAG_FMT_BUF_SIZE 256
|
|
#define BPF_DIAG_EVENT_LOG_MAX_SIZE (64U << 20)
|
|
#define DISASM_LINE_LEN 160
|
|
|
|
enum bpf_diag_mod_target_kind {
|
|
BPF_DIAG_MOD_TARGET_NONE,
|
|
BPF_DIAG_MOD_TARGET_REG,
|
|
BPF_DIAG_MOD_TARGET_STACK_ARG,
|
|
BPF_DIAG_MOD_TARGET_STACK_SLOT,
|
|
BPF_DIAG_MOD_TARGET_STACK_RANGE,
|
|
};
|
|
|
|
struct bpf_diag_mod_target {
|
|
u32 frame_id;
|
|
union {
|
|
struct {
|
|
s16 min_off;
|
|
s16 max_off;
|
|
} range;
|
|
u16 spi;
|
|
u8 regno;
|
|
u8 stack_arg;
|
|
};
|
|
u8 frameno;
|
|
u8 kind;
|
|
};
|
|
|
|
static struct bpf_diag_mod_target diag_reg_target(u32 frame_id, u8 frameno, u8 regno)
|
|
{
|
|
return (struct bpf_diag_mod_target){
|
|
.frame_id = frame_id,
|
|
.frameno = frameno,
|
|
.kind = BPF_DIAG_MOD_TARGET_REG,
|
|
.regno = regno,
|
|
};
|
|
}
|
|
|
|
static struct bpf_diag_mod_target diag_stack_arg_target(u32 frame_id, u8 frameno, u8 slot)
|
|
{
|
|
return (struct bpf_diag_mod_target){
|
|
.frame_id = frame_id,
|
|
.frameno = frameno,
|
|
.kind = BPF_DIAG_MOD_TARGET_STACK_ARG,
|
|
.stack_arg = slot,
|
|
};
|
|
}
|
|
|
|
static struct bpf_diag_mod_target diag_stack_slot_target(u32 frame_id, u8 frameno, u16 spi)
|
|
{
|
|
return (struct bpf_diag_mod_target){
|
|
.frame_id = frame_id,
|
|
.frameno = frameno,
|
|
.kind = BPF_DIAG_MOD_TARGET_STACK_SLOT,
|
|
.spi = spi,
|
|
};
|
|
}
|
|
|
|
static struct bpf_diag_mod_target diag_stack_range_target(u32 frame_id, u8 frameno,
|
|
s16 min_off, s16 max_off)
|
|
{
|
|
return (struct bpf_diag_mod_target){
|
|
.frame_id = frame_id,
|
|
.frameno = frameno,
|
|
.kind = BPF_DIAG_MOD_TARGET_STACK_RANGE,
|
|
.range.min_off = min_off,
|
|
.range.max_off = max_off,
|
|
};
|
|
}
|
|
|
|
struct bpf_diag_reg_snapshot {
|
|
u32 type;
|
|
u32 btf_id;
|
|
const struct bpf_map *map_ptr;
|
|
const struct btf *btf;
|
|
struct tnum var_off;
|
|
struct cnum64 r64;
|
|
};
|
|
|
|
enum bpf_diag_history_kind {
|
|
BPF_DIAG_HISTORY_BRANCH,
|
|
BPF_DIAG_HISTORY_MOD,
|
|
BPF_DIAG_HISTORY_REF_ACQUIRE,
|
|
BPF_DIAG_HISTORY_REF_RELEASE,
|
|
BPF_DIAG_HISTORY_CONTEXT,
|
|
};
|
|
|
|
struct bpf_diag_history_event {
|
|
u32 insn_idx : 24;
|
|
u32 kind : 8;
|
|
u8 in_lineage : 1;
|
|
union {
|
|
struct {
|
|
bool cond_true;
|
|
} branch;
|
|
struct {
|
|
struct bpf_diag_mod_target target;
|
|
struct bpf_diag_mod_target origin;
|
|
struct bpf_diag_reg_snapshot old, new;
|
|
u8 reason;
|
|
bool origin_valid;
|
|
} mod;
|
|
struct {
|
|
u32 ref_id;
|
|
} ref;
|
|
struct {
|
|
u32 depth;
|
|
u8 kind;
|
|
bool enter;
|
|
} ctx;
|
|
};
|
|
};
|
|
|
|
enum bpf_diag_history_scope {
|
|
BPF_DIAG_HISTORY_SCOPE_REG,
|
|
BPF_DIAG_HISTORY_SCOPE_STACK_ARG,
|
|
BPF_DIAG_HISTORY_SCOPE_REF,
|
|
BPF_DIAG_HISTORY_SCOPE_CONTEXT,
|
|
};
|
|
|
|
struct bpf_diag_history_opts {
|
|
enum bpf_diag_history_scope scope;
|
|
u32 frame_id;
|
|
u32 frameno;
|
|
int regno;
|
|
int stack_arg_slot;
|
|
u32 ref_id;
|
|
enum bpf_diag_context_kind ctx_kind;
|
|
u32 ctx_depth;
|
|
};
|
|
|
|
static void diag_print_history(struct bpf_verifier_env *env,
|
|
const struct bpf_diag_history_opts *opts);
|
|
static bool diag_target_matches(const struct bpf_diag_mod_target *event_target,
|
|
const struct bpf_diag_mod_target *target);
|
|
static const char *diag_context_name(enum bpf_diag_context_kind kind);
|
|
struct disasm_line {
|
|
char text[DISASM_LINE_LEN];
|
|
int idx;
|
|
bool valid;
|
|
};
|
|
|
|
struct disasm_ctx {
|
|
struct bpf_verifier_env *env;
|
|
struct seq_buf seq;
|
|
};
|
|
|
|
struct diag_fmt_chunk {
|
|
struct list_head node;
|
|
struct seq_buf seq;
|
|
char data[];
|
|
};
|
|
|
|
struct diag_fmt_mark {
|
|
struct diag_fmt_chunk *chunk;
|
|
size_t len;
|
|
};
|
|
|
|
struct bpf_diag_log {
|
|
struct bpf_diag_history_event *events;
|
|
/* Sequence number of the oldest retained event on the active path. */
|
|
u64 first_seq;
|
|
u32 cnt;
|
|
u32 cap;
|
|
u32 head;
|
|
bool growth_failed;
|
|
};
|
|
|
|
struct bpf_diag_scratch {
|
|
struct bpf_linfo_source source_lines[BPF_DIAG_CONTEXT_CNT];
|
|
struct disasm_line disasm_lines[BPF_DIAG_CONTEXT_CNT];
|
|
};
|
|
|
|
struct bpf_diag_mod_scope {
|
|
struct bpf_reg_state target_reg_snapshot;
|
|
struct bpf_diag_mod_target target;
|
|
struct bpf_diag_mod_target origin;
|
|
enum bpf_diag_mod_reason reason;
|
|
u32 insn_idx;
|
|
bool active;
|
|
bool origin_valid;
|
|
};
|
|
|
|
struct bpf_diag {
|
|
struct bpf_diag_log log;
|
|
struct bpf_diag_scratch scratch;
|
|
struct list_head fmt_chunks;
|
|
struct bpf_diag_mod_scope mod;
|
|
u32 frame_id_gen;
|
|
};
|
|
|
|
bool bpf_diag_enabled(const struct bpf_verifier_env *env)
|
|
{
|
|
return env->log.level & BPF_LOG_LEVEL;
|
|
}
|
|
|
|
static void diag_write(struct bpf_verifier_env *env, const char *fmt, ...) __printf(2, 3);
|
|
|
|
int bpf_diag_init(struct bpf_verifier_env *env)
|
|
{
|
|
if (!bpf_diag_enabled(env))
|
|
return 0;
|
|
|
|
env->diag = kzalloc_obj(struct bpf_diag, GFP_KERNEL_ACCOUNT);
|
|
if (!env->diag)
|
|
return -ENOMEM;
|
|
|
|
INIT_LIST_HEAD(&env->diag->fmt_chunks);
|
|
return 0;
|
|
}
|
|
|
|
void bpf_diag_init_frame(struct bpf_verifier_env *env, struct bpf_func_state *state)
|
|
{
|
|
if (env->diag)
|
|
state->diag_frame_id = ++env->diag->frame_id_gen;
|
|
}
|
|
|
|
static char *diag_fmt_alloc(struct bpf_verifier_env *env, size_t size)
|
|
{
|
|
struct bpf_diag *diag = env->diag;
|
|
struct diag_fmt_chunk *chunk;
|
|
size_t capacity, available;
|
|
char *buf;
|
|
|
|
if (!diag || !size || size > INT_MAX)
|
|
return NULL;
|
|
|
|
if (!list_empty(&diag->fmt_chunks)) {
|
|
chunk = list_last_entry(&diag->fmt_chunks, struct diag_fmt_chunk, node);
|
|
available = seq_buf_get_buf(&chunk->seq, &buf);
|
|
if (available >= size)
|
|
goto commit;
|
|
}
|
|
|
|
capacity = max_t(size_t, BPF_DIAG_FMT_CHUNK_SIZE, size);
|
|
chunk = kmalloc(struct_size(chunk, data, capacity), GFP_KERNEL_ACCOUNT);
|
|
if (!chunk)
|
|
return NULL;
|
|
|
|
seq_buf_init(&chunk->seq, chunk->data, capacity);
|
|
list_add_tail(&chunk->node, &diag->fmt_chunks);
|
|
available = seq_buf_get_buf(&chunk->seq, &buf);
|
|
if (WARN_ON_ONCE(available < size))
|
|
return NULL;
|
|
|
|
commit:
|
|
seq_buf_commit(&chunk->seq, size);
|
|
return buf;
|
|
}
|
|
|
|
char *bpf_diag_fmt_buf(struct bpf_verifier_env *env, size_t size)
|
|
{
|
|
char *buf;
|
|
|
|
buf = diag_fmt_alloc(env, size);
|
|
if (buf)
|
|
buf[0] = '\0';
|
|
return buf;
|
|
}
|
|
|
|
const char *bpf_diag_vfmt(struct bpf_verifier_env *env, const char *fmt, va_list args)
|
|
{
|
|
va_list copy;
|
|
char *buf;
|
|
int len;
|
|
|
|
va_copy(copy, args);
|
|
len = vsnprintf(NULL, 0, fmt, copy);
|
|
va_end(copy);
|
|
if (len < 0 || len == INT_MAX)
|
|
return "";
|
|
|
|
buf = diag_fmt_alloc(env, len + 1);
|
|
if (buf)
|
|
vsnprintf(buf, len + 1, fmt, args);
|
|
return buf ?: "";
|
|
}
|
|
|
|
const char *bpf_diag_fmt(struct bpf_verifier_env *env, const char *fmt, ...)
|
|
{
|
|
const char *buf;
|
|
va_list args;
|
|
|
|
va_start(args, fmt);
|
|
buf = bpf_diag_vfmt(env, fmt, args);
|
|
va_end(args);
|
|
return buf;
|
|
}
|
|
|
|
static struct diag_fmt_mark diag_fmt_save(struct bpf_verifier_env *env)
|
|
{
|
|
struct bpf_diag *diag = env->diag;
|
|
struct diag_fmt_mark mark = {};
|
|
|
|
if (!diag || list_empty(&diag->fmt_chunks))
|
|
return mark;
|
|
|
|
mark.chunk = list_last_entry(&diag->fmt_chunks, struct diag_fmt_chunk, node);
|
|
mark.len = mark.chunk->seq.len;
|
|
return mark;
|
|
}
|
|
|
|
static void diag_fmt_restore(struct bpf_verifier_env *env, struct diag_fmt_mark mark)
|
|
{
|
|
struct bpf_diag *diag = env->diag;
|
|
struct diag_fmt_chunk *chunk;
|
|
|
|
if (!diag)
|
|
return;
|
|
|
|
while (!list_empty(&diag->fmt_chunks)) {
|
|
chunk = list_last_entry(&diag->fmt_chunks, struct diag_fmt_chunk, node);
|
|
if (chunk == mark.chunk)
|
|
break;
|
|
list_del(&chunk->node);
|
|
kfree(chunk);
|
|
}
|
|
|
|
if (mark.chunk) {
|
|
mark.chunk->seq.len = mark.len;
|
|
seq_buf_str(&mark.chunk->seq);
|
|
}
|
|
}
|
|
|
|
void bpf_diag_free(struct bpf_verifier_env *env)
|
|
{
|
|
struct bpf_diag *diag = env->diag;
|
|
|
|
if (!diag)
|
|
return;
|
|
|
|
diag_fmt_restore(env, (struct diag_fmt_mark){});
|
|
kvfree(diag->log.events);
|
|
kfree(diag);
|
|
env->diag = NULL;
|
|
}
|
|
|
|
static void diag_write(struct bpf_verifier_env *env, const char *fmt, ...)
|
|
{
|
|
va_list args;
|
|
|
|
if (!bpf_diag_enabled(env))
|
|
return;
|
|
|
|
va_start(args, fmt);
|
|
bpf_verifier_vlog(&env->log, fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
static u64 log_end(const struct bpf_diag_log *log)
|
|
{
|
|
return log->first_seq + log->cnt;
|
|
}
|
|
|
|
static u32 log_pos(const struct bpf_diag_log *log, u32 idx)
|
|
{
|
|
u32 pos = log->head + idx;
|
|
|
|
return pos < log->cap ? pos : pos - log->cap;
|
|
}
|
|
|
|
u64 bpf_diag_event_log_save(struct bpf_verifier_env *env)
|
|
{
|
|
struct bpf_diag *diag = env->diag;
|
|
|
|
return diag ? log_end(&diag->log) : 0;
|
|
}
|
|
|
|
void bpf_diag_event_log_restore(struct bpf_verifier_env *env, u64 log_pos)
|
|
{
|
|
struct bpf_diag *diag = env->diag;
|
|
struct bpf_diag_log *log;
|
|
u64 end_seq;
|
|
|
|
if (!diag)
|
|
return;
|
|
|
|
log = &diag->log;
|
|
end_seq = log_end(log);
|
|
if (WARN_ON_ONCE(log_pos > end_seq))
|
|
log_pos = end_seq;
|
|
|
|
/*
|
|
* A deep abandoned path may have rotated away the shared prefix. In
|
|
* that case, restart with an empty retained suffix and remember that
|
|
* every event before the restored mark is unavailable.
|
|
*/
|
|
if (log_pos <= log->first_seq) {
|
|
log->first_seq = log_pos;
|
|
log->head = 0;
|
|
log->cnt = 0;
|
|
return;
|
|
}
|
|
|
|
log->cnt = log_pos - log->first_seq;
|
|
}
|
|
|
|
u32 bpf_diag_irq_depth(const struct bpf_verifier_state *state)
|
|
{
|
|
u32 depth = 0;
|
|
int i;
|
|
|
|
for (i = 0; i < state->acquired_refs; i++) {
|
|
if (state->refs[i].type == REF_TYPE_IRQ)
|
|
depth++;
|
|
}
|
|
|
|
return depth;
|
|
}
|
|
|
|
static void diag_append_history(struct bpf_verifier_env *env,
|
|
const struct bpf_diag_history_event *event)
|
|
{
|
|
struct bpf_diag_history_event *events;
|
|
struct bpf_diag *diag = env->diag;
|
|
struct bpf_diag_log *log;
|
|
u32 cap, max_events;
|
|
|
|
if (!diag)
|
|
return;
|
|
log = &diag->log;
|
|
|
|
if (log->cnt < log->cap) {
|
|
log->events[log_pos(log, log->cnt++)] = *event;
|
|
return;
|
|
}
|
|
|
|
max_events = BPF_DIAG_EVENT_LOG_MAX_SIZE / sizeof(*events);
|
|
if (log->growth_failed || log->cap == max_events)
|
|
goto rotate;
|
|
|
|
cap = min(log->cap ? log->cap * 2 : 64, max_events);
|
|
events = kvrealloc(log->events, array_size(cap, sizeof(*events)), GFP_KERNEL_ACCOUNT);
|
|
if (!events) {
|
|
log->growth_failed = true;
|
|
goto rotate;
|
|
}
|
|
log->events = events;
|
|
log->cap = cap;
|
|
log->events[log->cnt++] = *event;
|
|
return;
|
|
|
|
rotate:
|
|
if (log->cap) {
|
|
log->events[log->head++] = *event;
|
|
if (log->head == log->cap)
|
|
log->head = 0;
|
|
}
|
|
log->first_seq++;
|
|
}
|
|
|
|
static void diag_print_wrapped_prefixed(struct bpf_verifier_env *env, const char *first_prefix,
|
|
const char *next_prefix, const char *text)
|
|
{
|
|
const char *prefix = first_prefix;
|
|
|
|
while (*text) {
|
|
const char *line = text;
|
|
int prefix_len = strlen(prefix);
|
|
int text_width = BPF_DIAG_TEXT_WIDTH - prefix_len;
|
|
int len = 0, last_space = -1;
|
|
|
|
if (text_width < 1)
|
|
text_width = 1;
|
|
|
|
while (line[len] && line[len] != '\n' && len < text_width) {
|
|
if (line[len] == ' ')
|
|
last_space = len;
|
|
len++;
|
|
}
|
|
|
|
if (line[len] && line[len] != '\n' && line[len] != ' ' && last_space > 0)
|
|
len = last_space;
|
|
|
|
diag_write(env, "%s%.*s\n", prefix, len, line);
|
|
|
|
text = line + len;
|
|
while (*text == ' ')
|
|
text++;
|
|
if (*text == '\n')
|
|
text++;
|
|
|
|
prefix = next_prefix;
|
|
}
|
|
}
|
|
|
|
const char *bpf_diag_fmt_btf_type(struct bpf_verifier_env *env, const struct btf *btf, u32 type_id)
|
|
{
|
|
char *buf = bpf_diag_fmt_buf(env, BPF_DIAG_FMT_BUF_SIZE);
|
|
size_t len;
|
|
int ret;
|
|
|
|
if (!buf)
|
|
return "";
|
|
|
|
buf[0] = '\0';
|
|
ret = btf_type_name_to_buf(btf, type_id, buf, BPF_DIAG_FMT_BUF_SIZE);
|
|
if (ret < 0 || !buf[0]) {
|
|
scnprintf(buf, BPF_DIAG_FMT_BUF_SIZE, "BTF type ID %u", type_id);
|
|
return buf;
|
|
}
|
|
|
|
len = strlen(buf);
|
|
if (len && buf[len - 1] == '{')
|
|
buf[len - 1] = '\0';
|
|
return buf;
|
|
}
|
|
|
|
static void diag_vprint_indented(struct bpf_verifier_env *env, const char *fmt, va_list args)
|
|
__printf(2, 0);
|
|
|
|
static void diag_vprint_indented(struct bpf_verifier_env *env, const char *fmt, va_list args)
|
|
{
|
|
char *buf;
|
|
|
|
if (!bpf_diag_enabled(env))
|
|
return;
|
|
|
|
buf = kvasprintf(GFP_KERNEL_ACCOUNT, fmt, args);
|
|
if (!buf) {
|
|
diag_write(env, "%s<failed to allocate diagnostic text>\n", BPF_DIAG_TEXT_INDENT);
|
|
return;
|
|
}
|
|
|
|
diag_print_wrapped_prefixed(env, BPF_DIAG_TEXT_INDENT, BPF_DIAG_TEXT_INDENT, buf);
|
|
kfree(buf);
|
|
}
|
|
|
|
static int diag_line_width(unsigned int line)
|
|
{
|
|
int width = 1;
|
|
|
|
while (line >= 10) {
|
|
line /= 10;
|
|
width++;
|
|
}
|
|
|
|
return width;
|
|
}
|
|
|
|
static int diag_line_indent(const char *line)
|
|
{
|
|
int indent = 0;
|
|
|
|
while (*line == ' ' || *line == '\t') {
|
|
if (*line == '\t')
|
|
indent = round_up(indent + 1, BPF_DIAG_TAB_WIDTH);
|
|
else
|
|
indent++;
|
|
line++;
|
|
}
|
|
|
|
return indent;
|
|
}
|
|
|
|
static void disasm_print(void *private_data, const char *fmt, ...) __printf(2, 3);
|
|
|
|
static void disasm_print(void *private_data, const char *fmt, ...)
|
|
{
|
|
struct disasm_ctx *ctx = private_data;
|
|
va_list args;
|
|
|
|
va_start(args, fmt);
|
|
seq_buf_vprintf(&ctx->seq, fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
static const char *disasm_kfunc_name(void *private_data, const struct bpf_insn *insn)
|
|
{
|
|
struct disasm_ctx *ctx = private_data;
|
|
|
|
return bpf_disasm_kfunc_name(ctx->env, insn);
|
|
}
|
|
|
|
static void format_disasm_line(struct bpf_verifier_env *env, int insn_idx,
|
|
struct disasm_line *line)
|
|
{
|
|
struct disasm_ctx ctx = { .env = env };
|
|
struct bpf_insn *insn;
|
|
const struct bpf_insn_cbs cbs = {
|
|
.cb_call = disasm_kfunc_name,
|
|
.cb_print = disasm_print,
|
|
.private_data = &ctx,
|
|
};
|
|
|
|
line->idx = insn_idx;
|
|
line->valid = false;
|
|
seq_buf_init(&ctx.seq, line->text, sizeof(line->text));
|
|
|
|
if (insn_idx < 0 || insn_idx >= env->prog->len)
|
|
return;
|
|
|
|
if (insn_idx > 0 && bpf_is_ldimm64(&env->prog->insnsi[insn_idx - 1]))
|
|
return;
|
|
|
|
insn = &env->prog->insnsi[insn_idx];
|
|
if (bpf_is_ldimm64(insn) && insn_idx + 1 >= env->prog->len)
|
|
return;
|
|
|
|
print_bpf_insn(&cbs, insn, env->allow_ptr_leaks);
|
|
seq_buf_str(&ctx.seq);
|
|
ctx.seq.len = strnlen(line->text, sizeof(line->text));
|
|
while (ctx.seq.len && line->text[ctx.seq.len - 1] == '\n')
|
|
seq_buf_pop(&ctx.seq);
|
|
seq_buf_str(&ctx.seq);
|
|
|
|
line->valid = true;
|
|
}
|
|
|
|
static void diag_format_source_text(char *buf, size_t size, const char *line, int width)
|
|
{
|
|
int col = 0, len = 0;
|
|
|
|
if (!size)
|
|
return;
|
|
if (width <= 0) {
|
|
buf[0] = '\0';
|
|
return;
|
|
}
|
|
|
|
line = line ?: "...";
|
|
while (*line && col < width && len + 1 < size) {
|
|
if (*line == '\t') {
|
|
int next = round_up(col + 1, BPF_DIAG_TAB_WIDTH);
|
|
|
|
while (col < next && col < width && len + 1 < size) {
|
|
buf[len++] = ' ';
|
|
col++;
|
|
}
|
|
line++;
|
|
continue;
|
|
}
|
|
|
|
buf[len++] = *line++;
|
|
col++;
|
|
}
|
|
|
|
if (*line) {
|
|
int ellipsis_len = min(3, width);
|
|
|
|
while (len > 0 && col > width - ellipsis_len) {
|
|
len--;
|
|
col--;
|
|
}
|
|
while (ellipsis_len-- && len + 1 < size)
|
|
buf[len++] = '.';
|
|
}
|
|
|
|
buf[len] = '\0';
|
|
}
|
|
|
|
static void diag_format_source_lane(char *buf, size_t size, const char *source_prefix,
|
|
int source_line_width, int line_num, const char *line)
|
|
{
|
|
int len, text_width;
|
|
|
|
if (line_num <= 0) {
|
|
buf[0] = '\0';
|
|
return;
|
|
}
|
|
|
|
len = scnprintf(buf, size, "%s%*d | ", source_prefix, source_line_width, line_num);
|
|
text_width = BPF_DIAG_SOURCE_LANE_WIDTH - len;
|
|
diag_format_source_text(buf + len, size - len, line, text_width);
|
|
}
|
|
|
|
static void bpf_diag_header(struct bpf_verifier_env *env, const char *category,
|
|
const char *problem)
|
|
{
|
|
char first;
|
|
|
|
if (!bpf_diag_enabled(env))
|
|
return;
|
|
|
|
category = category ?: "Verifier Error";
|
|
problem = problem ?: "";
|
|
|
|
if (!problem[0]) {
|
|
diag_write(env, "\nVerification failed: %s\n", category);
|
|
return;
|
|
}
|
|
|
|
first = toupper(problem[0]);
|
|
diag_write(env, "\nVerification failed: %s: %c%s\n", category, first, problem + 1);
|
|
}
|
|
|
|
static void diag_reason(struct bpf_verifier_env *env, const char *fmt, ...) __printf(2, 3);
|
|
static void diag_suggestion(struct bpf_verifier_env *env, const char *fmt, ...)
|
|
__printf(2, 3);
|
|
|
|
static void diag_section(struct bpf_verifier_env *env, const char *title)
|
|
{
|
|
if (!bpf_diag_enabled(env))
|
|
return;
|
|
|
|
diag_write(env, "\n%s:\n", title);
|
|
}
|
|
|
|
static void diag_reason(struct bpf_verifier_env *env, const char *fmt, ...)
|
|
{
|
|
va_list args;
|
|
|
|
if (!bpf_diag_enabled(env))
|
|
return;
|
|
|
|
diag_section(env, "Reason");
|
|
|
|
va_start(args, fmt);
|
|
diag_vprint_indented(env, fmt, args);
|
|
va_end(args);
|
|
}
|
|
|
|
static void diag_suggestion(struct bpf_verifier_env *env, const char *fmt, ...)
|
|
{
|
|
va_list args;
|
|
|
|
if (!bpf_diag_enabled(env))
|
|
return;
|
|
|
|
diag_section(env, "Suggestion");
|
|
|
|
va_start(args, fmt);
|
|
diag_vprint_indented(env, fmt, args);
|
|
va_end(args);
|
|
diag_write(env, "\n");
|
|
}
|
|
|
|
static void diag_print_source_annotation(struct bpf_verifier_env *env, int line_width, int indent,
|
|
const char *label, const char *msg)
|
|
{
|
|
const char *first_prefix, *next_prefix, *text;
|
|
|
|
indent = min_t(int, indent, max_t(int, 0, BPF_DIAG_SOURCE_LANE_WIDTH - line_width - 8));
|
|
text = bpf_diag_fmt(env, "%s: %s", label, msg);
|
|
first_prefix = bpf_diag_fmt(env, " %*s | %*s^-- ", line_width + 4, "", indent, "");
|
|
next_prefix = bpf_diag_fmt(env, " %*s | %*s ", line_width + 4, "", indent, "");
|
|
|
|
diag_print_wrapped_prefixed(env, first_prefix, next_prefix, text);
|
|
}
|
|
|
|
static void diag_print_insn_context(struct bpf_verifier_env *env, u32 insn_idx,
|
|
struct disasm_line *disasm_lines)
|
|
{
|
|
int insn_width = diag_line_width(env->prog->len ? env->prog->len - 1 : 0);
|
|
int i;
|
|
|
|
for (i = 0; i < BPF_DIAG_CONTEXT_CNT; i++) {
|
|
int row = i - BPF_DIAG_CONTEXT;
|
|
|
|
format_disasm_line(env, insn_idx + row, &disasm_lines[i]);
|
|
}
|
|
|
|
diag_write(env, " Instruction context:\n");
|
|
for (i = 0; i < BPF_DIAG_CONTEXT_CNT; i++) {
|
|
struct disasm_line *line = &disasm_lines[i];
|
|
|
|
if (line->valid)
|
|
diag_write(env, " %s%*d | %s\n",
|
|
line->idx == insn_idx ? ">>> " : " ",
|
|
insn_width, line->idx, line->text);
|
|
}
|
|
}
|
|
|
|
static void bpf_diag_source(struct bpf_verifier_env *env, u32 insn_idx, const char *label,
|
|
const char *fmt, ...)
|
|
{
|
|
struct bpf_diag_scratch *scratch;
|
|
struct bpf_linfo_source *source_lines;
|
|
struct disasm_line *disasm_lines;
|
|
struct bpf_linfo_source src = {};
|
|
struct diag_fmt_mark mark;
|
|
const struct bpf_line_info *linfo;
|
|
const struct bpf_subprog_info *subprog;
|
|
struct btf *btf = env->prog->aux->btf;
|
|
char *source_lane;
|
|
const char *msg;
|
|
const char *func;
|
|
int start_line, end_line, width, indent, subprogno, linfo_start, linfo_end, i;
|
|
va_list args;
|
|
|
|
if (!bpf_diag_enabled(env))
|
|
return;
|
|
if (!env->diag)
|
|
return;
|
|
|
|
mark = diag_fmt_save(env);
|
|
label = label ?: "note";
|
|
scratch = &env->diag->scratch;
|
|
source_lines = scratch->source_lines;
|
|
disasm_lines = scratch->disasm_lines;
|
|
memset(source_lines, 0, sizeof(scratch->source_lines));
|
|
memset(disasm_lines, 0, sizeof(scratch->disasm_lines));
|
|
|
|
va_start(args, fmt);
|
|
msg = bpf_diag_vfmt(env, fmt, args);
|
|
va_end(args);
|
|
if (!*msg)
|
|
msg = "<failed to allocate diagnostic text>";
|
|
|
|
linfo = bpf_find_linfo(env->prog, insn_idx);
|
|
if (btf && linfo)
|
|
bpf_get_linfo_source(btf, linfo, &src);
|
|
if (!src.file || !*src.file) {
|
|
diag_write(env, " insn %u\n", insn_idx);
|
|
goto out_annotation;
|
|
}
|
|
|
|
subprog = bpf_find_containing_subprog(env, insn_idx);
|
|
subprogno = subprog ? subprog - env->subprog_info : -ENOENT;
|
|
func = subprogno >= 0 ? bpf_subprog_name(env, subprogno) : NULL;
|
|
if (func && *func)
|
|
diag_write(env, " %s @ %s:%d:%d\n", func, src.file, src.line_num, src.line_col);
|
|
else
|
|
diag_write(env, " %s:%d:%d\n", src.file, src.line_num, src.line_col);
|
|
if (!src.line || !*src.line)
|
|
goto out_annotation;
|
|
|
|
start_line = src.line_num - BPF_DIAG_CONTEXT;
|
|
end_line = src.line_num + BPF_DIAG_CONTEXT;
|
|
width = diag_line_width(end_line);
|
|
indent = diag_line_indent(src.line);
|
|
for (i = 0; i < BPF_DIAG_CONTEXT_CNT; i++)
|
|
source_lines[i].line_num = start_line + i;
|
|
|
|
linfo = env->prog->aux->linfo;
|
|
linfo_start = subprog ? subprog->linfo_idx : 0;
|
|
linfo_end = subprogno >= 0 && subprogno + 1 < env->subprog_cnt ?
|
|
env->subprog_info[subprogno + 1].linfo_idx : env->prog->aux->nr_linfo;
|
|
for (i = linfo_start; i < linfo_end; i++) {
|
|
struct bpf_linfo_source line_src;
|
|
int idx;
|
|
|
|
bpf_get_linfo_source(btf, &linfo[i], &line_src);
|
|
if (line_src.file_name_off != src.file_name_off ||
|
|
line_src.line_num < start_line || line_src.line_num > end_line ||
|
|
!line_src.line || !*line_src.line)
|
|
continue;
|
|
|
|
idx = line_src.line_num - start_line;
|
|
if (!source_lines[idx].line)
|
|
source_lines[idx] = line_src;
|
|
}
|
|
|
|
diag_write(env, " Source context:\n");
|
|
source_lane = bpf_diag_fmt_buf(env, BPF_DIAG_FMT_BUF_SIZE);
|
|
if (!source_lane)
|
|
goto out_restore;
|
|
for (i = 0; i < BPF_DIAG_CONTEXT_CNT; i++) {
|
|
const char *source_prefix;
|
|
|
|
source_prefix = source_lines[i].line_num == src.line_num ? ">>> " : " ";
|
|
diag_format_source_lane(source_lane, BPF_DIAG_FMT_BUF_SIZE, source_prefix, width,
|
|
source_lines[i].line_num, source_lines[i].line);
|
|
diag_write(env, " %s\n", source_lane);
|
|
if (source_lines[i].line_num == src.line_num)
|
|
diag_print_source_annotation(env, width, indent, label, msg);
|
|
}
|
|
diag_print_insn_context(env, insn_idx, disasm_lines);
|
|
goto out_restore;
|
|
|
|
out_annotation:
|
|
diag_print_source_annotation(env, 0, 0, label, msg);
|
|
diag_print_insn_context(env, insn_idx, disasm_lines);
|
|
out_restore:
|
|
diag_fmt_restore(env, mark);
|
|
}
|
|
|
|
static const struct bpf_func_state *diag_current_frame(const struct bpf_verifier_env *env)
|
|
{
|
|
return env->cur_state->frame[env->cur_state->curframe];
|
|
}
|
|
|
|
void bpf_diag_register_type(struct bpf_verifier_env *env, u32 insn_idx, int regno,
|
|
const char *problem, const char *reason, const char *suggestion)
|
|
{
|
|
const struct bpf_func_state *frame = diag_current_frame(env);
|
|
struct bpf_diag_history_opts opts = {
|
|
.scope = BPF_DIAG_HISTORY_SCOPE_REG,
|
|
.frame_id = frame->diag_frame_id,
|
|
.frameno = frame->frameno,
|
|
.regno = regno,
|
|
};
|
|
|
|
bpf_diag_header(env, REGISTER_TYPE_SAFETY, problem);
|
|
diag_reason(env, "%s", reason);
|
|
|
|
diag_section(env, "At");
|
|
bpf_diag_source(env, insn_idx, "error", "%s", problem);
|
|
|
|
if (regno >= 0)
|
|
diag_print_history(env, &opts);
|
|
|
|
diag_suggestion(env, "%s", suggestion);
|
|
}
|
|
|
|
const char *bpf_diag_reg_type_plain(struct bpf_verifier_env *env, enum bpf_reg_type type)
|
|
{
|
|
switch (base_type(type)) {
|
|
case NOT_INIT:
|
|
return "an uninitialized value";
|
|
case SCALAR_VALUE:
|
|
return "an integer scalar";
|
|
case PTR_TO_CTX:
|
|
return "a context pointer";
|
|
case PTR_TO_STACK:
|
|
return "a stack pointer";
|
|
case PTR_TO_MAP_VALUE:
|
|
if (type_may_be_null(type))
|
|
return "a nullable map value pointer";
|
|
return "a map value pointer";
|
|
case PTR_TO_MEM:
|
|
if (type_may_be_null(type))
|
|
return "a nullable memory pointer";
|
|
return "a memory pointer";
|
|
case PTR_TO_BTF_ID:
|
|
if (type_may_be_null(type))
|
|
return "a nullable kernel object pointer";
|
|
if (type_is_non_owning_ref(type))
|
|
return "a borrowed allocated object pointer";
|
|
if (type_is_ptr_alloc_obj(type))
|
|
return "an owned allocated object pointer";
|
|
if (type_flag(type) & PTR_UNTRUSTED)
|
|
return "an untrusted kernel object pointer";
|
|
return "a kernel object pointer";
|
|
default:
|
|
return reg_type_str(env, type);
|
|
}
|
|
}
|
|
|
|
static const char *diag_arg_ordinal(int argno)
|
|
{
|
|
switch (argno) {
|
|
case 1:
|
|
return "first";
|
|
case 2:
|
|
return "second";
|
|
case 3:
|
|
return "third";
|
|
case 4:
|
|
return "fourth";
|
|
case 5:
|
|
return "fifth";
|
|
case 6:
|
|
return "sixth";
|
|
case 7:
|
|
return "seventh";
|
|
case 8:
|
|
return "eighth";
|
|
case 9:
|
|
return "ninth";
|
|
case 10:
|
|
return "tenth";
|
|
case 11:
|
|
return "eleventh";
|
|
case 12:
|
|
return "twelfth";
|
|
default:
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
void bpf_diag_call_type(struct bpf_verifier_env *env, u32 insn_idx, int argno, int regno,
|
|
int stack_arg_slot, const char *call_name, const char *arg_name,
|
|
const char *reason, const char *suggestion)
|
|
{
|
|
const struct bpf_func_state *frame = diag_current_frame(env);
|
|
struct bpf_diag_history_opts opts = {
|
|
.frame_id = frame->diag_frame_id,
|
|
.frameno = frame->frameno,
|
|
};
|
|
const char *ordinal = diag_arg_ordinal(argno);
|
|
const char *arg_desc;
|
|
bool print_history = true;
|
|
|
|
if (regno >= 0) {
|
|
opts.scope = BPF_DIAG_HISTORY_SCOPE_REG;
|
|
opts.regno = regno;
|
|
} else if (stack_arg_slot >= 0) {
|
|
opts.scope = BPF_DIAG_HISTORY_SCOPE_STACK_ARG;
|
|
opts.stack_arg_slot = stack_arg_slot;
|
|
} else {
|
|
print_history = false;
|
|
}
|
|
|
|
if (ordinal && arg_name)
|
|
arg_desc = bpf_diag_fmt(env, "%s argument (%s)", ordinal, arg_name);
|
|
else if (ordinal)
|
|
arg_desc = bpf_diag_fmt(env, "%s argument", ordinal);
|
|
else if (arg_name)
|
|
arg_desc = bpf_diag_fmt(env, "argument %s", arg_name);
|
|
else
|
|
arg_desc = "argument";
|
|
|
|
bpf_diag_header(env, CALL_TYPE_SAFETY, "invalid call argument");
|
|
diag_reason(env, "The %s to %s does not satisfy the verifier contract: %s.",
|
|
arg_desc, call_name, reason);
|
|
|
|
diag_section(env, "At");
|
|
bpf_diag_source(env, insn_idx, "error", "invalid %s for %s", arg_desc, call_name);
|
|
|
|
if (print_history)
|
|
diag_print_history(env, &opts);
|
|
|
|
diag_suggestion(env, "%s", suggestion);
|
|
}
|
|
|
|
static const char *diag_context_constraint(enum bpf_diag_context_kind kind)
|
|
{
|
|
switch (kind) {
|
|
case BPF_DIAG_CONTEXT_RCU:
|
|
return "RCU read-side critical sections cannot call operations that may sleep";
|
|
case BPF_DIAG_CONTEXT_PREEMPT:
|
|
return "preemption-disabled code cannot call operations that may sleep";
|
|
case BPF_DIAG_CONTEXT_IRQ:
|
|
return "IRQ-disabled code cannot call operations that may sleep";
|
|
case BPF_DIAG_CONTEXT_LOCK:
|
|
return "code holding a BPF spin lock cannot call operations that may sleep";
|
|
case BPF_DIAG_CONTEXT_NONE:
|
|
default:
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
static const char *diag_active_context(struct bpf_verifier_env *env, u32 depth,
|
|
const char *context)
|
|
{
|
|
if (depth == 1)
|
|
return bpf_diag_fmt(env, "an active %s (depth 1)", context);
|
|
return bpf_diag_fmt(env, "%u active %ss (depth %u)", depth, context, depth);
|
|
}
|
|
|
|
static u32 diag_context_depth(struct bpf_verifier_env *env, enum bpf_diag_context_kind kind)
|
|
{
|
|
switch (kind) {
|
|
case BPF_DIAG_CONTEXT_RCU:
|
|
return env->cur_state->active_rcu_locks;
|
|
case BPF_DIAG_CONTEXT_PREEMPT:
|
|
return env->cur_state->active_preempt_locks;
|
|
case BPF_DIAG_CONTEXT_IRQ:
|
|
return bpf_diag_irq_depth(env->cur_state);
|
|
case BPF_DIAG_CONTEXT_LOCK:
|
|
return env->cur_state->active_locks;
|
|
case BPF_DIAG_CONTEXT_NONE:
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
void bpf_diag_ctx_forbidden(struct bpf_verifier_env *env, u32 insn_idx,
|
|
const char *operation, const char *suggestion)
|
|
{
|
|
struct bpf_diag_history_opts opts;
|
|
enum bpf_diag_context_kind ctx_kind;
|
|
const char *constraint, *context;
|
|
u32 depth;
|
|
|
|
if (env->cur_state->active_rcu_locks)
|
|
ctx_kind = BPF_DIAG_CONTEXT_RCU;
|
|
else if (env->cur_state->active_preempt_locks)
|
|
ctx_kind = BPF_DIAG_CONTEXT_PREEMPT;
|
|
else if (env->cur_state->active_irq_id)
|
|
ctx_kind = BPF_DIAG_CONTEXT_IRQ;
|
|
else if (env->cur_state->active_locks)
|
|
ctx_kind = BPF_DIAG_CONTEXT_LOCK;
|
|
else
|
|
ctx_kind = BPF_DIAG_CONTEXT_NONE;
|
|
|
|
depth = diag_context_depth(env, ctx_kind);
|
|
opts = (struct bpf_diag_history_opts) {
|
|
.scope = BPF_DIAG_HISTORY_SCOPE_CONTEXT,
|
|
.ctx_kind = ctx_kind,
|
|
.ctx_depth = depth,
|
|
};
|
|
constraint = diag_context_constraint(ctx_kind);
|
|
context = diag_context_name(ctx_kind);
|
|
|
|
bpf_diag_header(env, EXECUTION_CONTEXT_SAFETY,
|
|
"operation is not allowed in this context");
|
|
if (constraint) {
|
|
if (depth) {
|
|
diag_reason(
|
|
env, "The operation %s cannot be used in %s because %s. This path is still inside %s.",
|
|
operation, context, constraint, diag_active_context(env, depth, context));
|
|
} else {
|
|
diag_reason(env, "The operation %s cannot be used in %s because %s.",
|
|
operation, context, constraint);
|
|
}
|
|
} else {
|
|
diag_reason(env, "The operation %s cannot be used in %s.", operation,
|
|
context);
|
|
}
|
|
|
|
diag_section(env, "At");
|
|
bpf_diag_source(env, insn_idx, "error", "%s is not allowed in %s", operation,
|
|
context);
|
|
|
|
if (ctx_kind != BPF_DIAG_CONTEXT_NONE)
|
|
diag_print_history(env, &opts);
|
|
|
|
diag_suggestion(env, "%s", suggestion);
|
|
}
|
|
|
|
void bpf_diag_ctx_active(struct bpf_verifier_env *env, u32 insn_idx, const char *operation,
|
|
enum bpf_diag_context_kind ctx_kind, const char *suggestion)
|
|
{
|
|
u32 depth = diag_context_depth(env, ctx_kind);
|
|
struct bpf_diag_history_opts opts = {
|
|
.scope = BPF_DIAG_HISTORY_SCOPE_CONTEXT,
|
|
.ctx_kind = ctx_kind,
|
|
.ctx_depth = depth,
|
|
};
|
|
const char *context = diag_context_name(ctx_kind);
|
|
|
|
bpf_diag_header(env, EXECUTION_CONTEXT_SAFETY,
|
|
"operation is not allowed in this context");
|
|
diag_reason(
|
|
env, "The operation %s cannot be used while this path is still inside %s. Leave the region before this operation.",
|
|
operation, diag_active_context(env, depth, context));
|
|
|
|
diag_section(env, "At");
|
|
bpf_diag_source(env, insn_idx, "error", "%s is not allowed before leaving %s",
|
|
operation, context);
|
|
|
|
diag_print_history(env, &opts);
|
|
|
|
diag_suggestion(env, "%s", suggestion);
|
|
}
|
|
|
|
void bpf_diag_ctx_required(struct bpf_verifier_env *env, u32 insn_idx, const char *operation,
|
|
enum bpf_diag_context_kind ctx_kind, const char *suggestion)
|
|
{
|
|
const char *context = diag_context_name(ctx_kind);
|
|
|
|
bpf_diag_header(env, EXECUTION_CONTEXT_SAFETY, "required context is not active");
|
|
diag_reason(env, "The operation %s requires an active %s, but this path is outside one.",
|
|
operation, context);
|
|
|
|
diag_section(env, "At");
|
|
bpf_diag_source(env, insn_idx, "error", "%s requires %s", operation, context);
|
|
|
|
diag_suggestion(env, "%s", suggestion);
|
|
}
|
|
|
|
void bpf_diag_ctx_underflow(struct bpf_verifier_env *env, u32 insn_idx,
|
|
const char *operation, enum bpf_diag_context_kind ctx_kind,
|
|
const char *suggestion)
|
|
{
|
|
struct bpf_diag_history_opts opts = {
|
|
.scope = BPF_DIAG_HISTORY_SCOPE_CONTEXT,
|
|
.ctx_kind = ctx_kind,
|
|
};
|
|
const char *context = diag_context_name(ctx_kind);
|
|
|
|
bpf_diag_header(env, EXECUTION_CONTEXT_SAFETY, "unmatched context exit");
|
|
diag_reason(
|
|
env, "The operation %s tries to leave %s, but this path has no active %s to leave. The current depth is 0.",
|
|
operation, context, context);
|
|
|
|
diag_section(env, "At");
|
|
bpf_diag_source(env, insn_idx, "error", "%s has no matching enter on this path",
|
|
operation);
|
|
|
|
diag_print_history(env, &opts);
|
|
|
|
diag_suggestion(env, "%s", suggestion);
|
|
}
|
|
|
|
void bpf_diag_program_structure(struct bpf_verifier_env *env, u32 insn_idx,
|
|
const char *problem, const char *suggestion,
|
|
const char *reason_fmt, ...)
|
|
{
|
|
va_list args;
|
|
|
|
bpf_diag_header(env, PROGRAM_STRUCTURE, problem);
|
|
diag_section(env, "Reason");
|
|
|
|
va_start(args, reason_fmt);
|
|
diag_vprint_indented(env, reason_fmt, args);
|
|
va_end(args);
|
|
|
|
diag_section(env, "At");
|
|
bpf_diag_source(env, insn_idx, "error", "%s", problem);
|
|
|
|
diag_suggestion(env, "%s", suggestion);
|
|
}
|
|
|
|
void bpf_diag_policy(struct bpf_verifier_env *env, u32 insn_idx, const char *operation,
|
|
const char *reason, const char *suggestion)
|
|
{
|
|
bpf_diag_header(env, POLICY, "operation is not allowed");
|
|
diag_reason(env, "The %s is not allowed: %s.", operation, reason);
|
|
|
|
diag_section(env, "At");
|
|
bpf_diag_source(env, insn_idx, "error", "policy check failed for %s", operation);
|
|
|
|
diag_suggestion(env, "%s", suggestion);
|
|
}
|
|
|
|
void bpf_diag_invalid_deref(struct bpf_verifier_env *env, u32 insn_idx, int regno,
|
|
const char *reg_name, const struct bpf_reg_state *reg,
|
|
enum bpf_diag_invalid_deref_kind kind, s64 offset)
|
|
{
|
|
const struct bpf_func_state *frame = diag_current_frame(env);
|
|
struct bpf_diag_history_opts opts = {
|
|
.scope = BPF_DIAG_HISTORY_SCOPE_REG,
|
|
.frame_id = frame->diag_frame_id,
|
|
.frameno = frame->frameno,
|
|
.regno = regno,
|
|
};
|
|
const char *type_name = bpf_diag_reg_type_plain(env, reg->type);
|
|
|
|
bpf_diag_header(env, REGISTER_TYPE_SAFETY, "invalid dereference");
|
|
|
|
switch (kind) {
|
|
case BPF_DIAG_DEREF_SCALAR:
|
|
diag_reason(env, "%s is an integer scalar here, not a pointer to memory.",
|
|
reg_name);
|
|
break;
|
|
case BPF_DIAG_DEREF_NULLABLE_PTR:
|
|
diag_reason(
|
|
env, "%s may be NULL here (%s). The program could dereference NULL on this path, so the verifier cannot prove this access is safe.",
|
|
reg_name, type_name);
|
|
break;
|
|
case BPF_DIAG_DEREF_MODIFIED_PTR:
|
|
diag_reason(
|
|
env, "%s has offset %lld here, but this pointer type must be dereferenced in its original form.",
|
|
reg_name, offset);
|
|
break;
|
|
case BPF_DIAG_DEREF_INVALID_PTR:
|
|
default:
|
|
diag_reason(
|
|
env, "%s has type %s here, which is not valid for this memory access.",
|
|
reg_name, type_name);
|
|
break;
|
|
}
|
|
|
|
diag_section(env, "At");
|
|
if (kind == BPF_DIAG_DEREF_MODIFIED_PTR)
|
|
bpf_diag_source(env, insn_idx, "error",
|
|
"dereference requires the original %s pointer", type_name);
|
|
else
|
|
bpf_diag_source(env, insn_idx, "error", "invalid dereference of %s (%s)",
|
|
reg_name, type_name);
|
|
|
|
if (regno >= 0)
|
|
diag_print_history(env, &opts);
|
|
|
|
switch (kind) {
|
|
case BPF_DIAG_DEREF_NULLABLE_PTR:
|
|
diag_suggestion(
|
|
env, "Add a NULL check before the access and dereference the pointer only on the non-NULL path.");
|
|
break;
|
|
case BPF_DIAG_DEREF_MODIFIED_PTR:
|
|
diag_suggestion(
|
|
env, "Preserve the original pointer in another register, or use only offsets this pointer type permits before dereferencing it.");
|
|
break;
|
|
case BPF_DIAG_DEREF_SCALAR:
|
|
case BPF_DIAG_DEREF_INVALID_PTR:
|
|
default:
|
|
diag_suggestion(
|
|
env, "Preserve a pointer-valued register where needed, or reload and revalidate the pointer after scalar arithmetic, helper calls, or other operations that can invalidate it.");
|
|
break;
|
|
}
|
|
}
|
|
|
|
void bpf_diag_unreadable_reg(struct bpf_verifier_env *env, u32 insn_idx, int regno)
|
|
{
|
|
const struct bpf_func_state *frame = diag_current_frame(env);
|
|
struct bpf_diag_history_opts opts = {
|
|
.scope = BPF_DIAG_HISTORY_SCOPE_REG,
|
|
.frame_id = frame->diag_frame_id,
|
|
.frameno = frame->frameno,
|
|
.regno = regno,
|
|
};
|
|
const struct bpf_diag_log *log = env->diag ? &env->diag->log : NULL;
|
|
struct bpf_diag_mod_target target;
|
|
bool invalidated = false;
|
|
int i;
|
|
|
|
target = diag_reg_target(opts.frame_id, opts.frameno, regno);
|
|
for (i = log ? log->cnt : 0; i > 0; i--) {
|
|
const struct bpf_diag_history_event *event;
|
|
|
|
event = &log->events[log_pos(log, i - 1)];
|
|
|
|
if (event->kind != BPF_DIAG_HISTORY_MOD ||
|
|
!diag_target_matches(&event->mod.target, &target))
|
|
continue;
|
|
invalidated = event->mod.new.type == NOT_INIT;
|
|
break;
|
|
}
|
|
|
|
bpf_diag_header(env, REGISTER_TYPE_SAFETY, "unreadable register");
|
|
if (invalidated)
|
|
diag_reason(
|
|
env, "R%d is not readable here. A previous operation invalidated this register, so the verifier cannot use it as an input.",
|
|
regno);
|
|
else if (log && !log->first_seq)
|
|
diag_reason(env,
|
|
"R%d has never been initialized on this path, so the verifier cannot use it as an input.",
|
|
regno);
|
|
else
|
|
diag_reason(
|
|
env, "R%d is not readable here. It may never have been initialized, or an earlier operation may have invalidated it.",
|
|
regno);
|
|
|
|
diag_section(env, "At");
|
|
bpf_diag_source(env, insn_idx, "error", "R%d is not readable", regno);
|
|
|
|
if (regno >= 0)
|
|
diag_print_history(env, &opts);
|
|
|
|
if (invalidated)
|
|
diag_suggestion(
|
|
env, "Avoid using the register after it is invalidated, or initialize it again before this instruction.");
|
|
else if (log && !log->first_seq)
|
|
diag_suggestion(env, "Initialize R%d on every path before this instruction.", regno);
|
|
else
|
|
diag_suggestion(
|
|
env, "Initialize the register on every path, or initialize it again after any operation that invalidates it.");
|
|
}
|
|
|
|
static int diag_stack_argno(u8 slot)
|
|
{
|
|
return MAX_BPF_FUNC_REG_ARGS + slot + 1;
|
|
}
|
|
|
|
static void diag_format_stack_arg(char *buf, size_t size, u8 slot, const char *arg_name)
|
|
{
|
|
int argno = diag_stack_argno(slot);
|
|
const char *ordinal = diag_arg_ordinal(argno);
|
|
|
|
if (ordinal && arg_name)
|
|
scnprintf(buf, size, "outgoing stack argument %u (%s argument, %s)", slot + 1,
|
|
ordinal, arg_name);
|
|
else if (ordinal)
|
|
scnprintf(buf, size, "outgoing stack argument %u (%s argument)", slot + 1, ordinal);
|
|
else if (arg_name)
|
|
scnprintf(buf, size, "outgoing stack argument %u (%s)", slot + 1, arg_name);
|
|
else
|
|
scnprintf(buf, size, "outgoing stack argument %u", slot + 1);
|
|
}
|
|
|
|
void bpf_diag_stack_arg_uninit(struct bpf_verifier_env *env, u32 insn_idx, int nargs,
|
|
int stack_arg_slot, const char *callee_name,
|
|
const char *arg_name)
|
|
{
|
|
const struct bpf_func_state *frame = diag_current_frame(env);
|
|
struct bpf_diag_history_opts opts = {
|
|
.scope = BPF_DIAG_HISTORY_SCOPE_STACK_ARG,
|
|
.frame_id = frame->diag_frame_id,
|
|
.frameno = frame->frameno,
|
|
.stack_arg_slot = stack_arg_slot,
|
|
};
|
|
const char *arg_buf;
|
|
|
|
arg_buf = bpf_diag_fmt_buf(env, BPF_DIAG_FMT_BUF_SIZE);
|
|
if (arg_buf)
|
|
diag_format_stack_arg((char *)arg_buf, BPF_DIAG_FMT_BUF_SIZE, stack_arg_slot,
|
|
arg_name);
|
|
else
|
|
arg_buf = "";
|
|
bpf_diag_header(env, REGISTER_TYPE_SAFETY, "missing stack argument");
|
|
if (callee_name && *callee_name)
|
|
diag_reason(
|
|
env, "Function %s expects %d arguments, but %s is not initialized at this call.",
|
|
callee_name, nargs, arg_buf);
|
|
else
|
|
diag_reason(
|
|
env, "The callee expects %d arguments, but %s is not initialized at this call.",
|
|
nargs, arg_buf);
|
|
|
|
diag_section(env, "At");
|
|
bpf_diag_source(env, insn_idx, "error", "%s is not initialized", arg_buf);
|
|
|
|
if (stack_arg_slot >= 0)
|
|
diag_print_history(env, &opts);
|
|
|
|
diag_suggestion(
|
|
env, "Write the outgoing stack argument after any operation that may invalidate stored pointer values, and before making this call.");
|
|
}
|
|
|
|
void bpf_diag_memory(struct bpf_verifier_env *env, u32 insn_idx, const char *problem,
|
|
const char *reason, const char *suggestion)
|
|
{
|
|
bpf_diag_header(env, MEMORY_SAFETY, problem);
|
|
diag_reason(env, "%s", reason);
|
|
|
|
diag_section(env, "At");
|
|
bpf_diag_source(env, insn_idx, "error", "%s", problem);
|
|
|
|
diag_suggestion(env, "%s", suggestion);
|
|
}
|
|
|
|
void bpf_diag_record_branch(struct bpf_verifier_env *env, u32 insn_idx, bool cond_true)
|
|
{
|
|
struct bpf_diag_history_event event = {
|
|
.insn_idx = insn_idx,
|
|
.kind = BPF_DIAG_HISTORY_BRANCH,
|
|
.branch = {
|
|
.cond_true = cond_true,
|
|
},
|
|
};
|
|
|
|
diag_append_history(env, &event);
|
|
}
|
|
|
|
static void diag_snapshot_reg(struct bpf_diag_reg_snapshot *snapshot,
|
|
const struct bpf_reg_state *reg)
|
|
{
|
|
snapshot->type = reg->type;
|
|
if (type_is_map_ptr(reg->type))
|
|
snapshot->map_ptr = reg->map_ptr;
|
|
if (base_type(reg->type) == PTR_TO_BTF_ID && reg->btf && reg->btf_id) {
|
|
snapshot->btf_id = reg->btf_id;
|
|
snapshot->btf = reg->btf;
|
|
}
|
|
snapshot->var_off = reg->var_off;
|
|
snapshot->r64 = reg->r64;
|
|
}
|
|
|
|
static bool diag_mod_insn_origin(struct bpf_verifier_env *env, u32 insn_idx,
|
|
const struct bpf_diag_mod_target *target,
|
|
struct bpf_diag_mod_target *origin)
|
|
{
|
|
const struct bpf_insn *insn = &env->prog->insnsi[insn_idx];
|
|
u8 class = BPF_CLASS(insn->code);
|
|
const struct bpf_func_state *state;
|
|
|
|
if (target->kind == BPF_DIAG_MOD_TARGET_REG && (class == BPF_ALU || class == BPF_ALU64) &&
|
|
BPF_OP(insn->code) == BPF_MOV && BPF_SRC(insn->code) == BPF_X) {
|
|
*origin = diag_reg_target(target->frame_id, target->frameno, insn->src_reg);
|
|
return true;
|
|
}
|
|
|
|
if ((target->kind != BPF_DIAG_MOD_TARGET_STACK_ARG &&
|
|
target->kind != BPF_DIAG_MOD_TARGET_STACK_SLOT) ||
|
|
class != BPF_STX)
|
|
return false;
|
|
|
|
state = env->cur_state->frame[env->cur_state->curframe];
|
|
*origin = diag_reg_target(state->diag_frame_id, state->frameno, insn->src_reg);
|
|
return true;
|
|
}
|
|
|
|
static bool diag_mod_keeps_lineage(struct bpf_verifier_env *env,
|
|
const struct bpf_diag_history_event *event)
|
|
{
|
|
const struct bpf_insn *insn;
|
|
u8 class;
|
|
|
|
if (event->mod.reason != BPF_DIAG_MOD_WRITE ||
|
|
event->mod.target.kind != BPF_DIAG_MOD_TARGET_REG)
|
|
return false;
|
|
|
|
insn = &env->prog->insnsi[event->insn_idx];
|
|
class = BPF_CLASS(insn->code);
|
|
if (class != BPF_ALU && class != BPF_ALU64)
|
|
return false;
|
|
|
|
switch (BPF_OP(insn->code)) {
|
|
case BPF_ADD:
|
|
case BPF_SUB:
|
|
case BPF_MUL:
|
|
case BPF_OR:
|
|
case BPF_AND:
|
|
case BPF_LSH:
|
|
case BPF_RSH:
|
|
case BPF_ARSH:
|
|
case BPF_XOR:
|
|
case BPF_NEG:
|
|
case BPF_END:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static void diag_record_mod(struct bpf_verifier_env *env, u32 insn_idx,
|
|
struct bpf_diag_mod_target target,
|
|
enum bpf_diag_mod_reason reason,
|
|
const struct bpf_reg_state *old_reg,
|
|
const struct bpf_reg_state *new_reg,
|
|
const struct bpf_diag_mod_target *origin)
|
|
{
|
|
struct bpf_diag_history_event event = {
|
|
.insn_idx = insn_idx,
|
|
.kind = BPF_DIAG_HISTORY_MOD,
|
|
.mod = {
|
|
.target = target,
|
|
.reason = reason,
|
|
},
|
|
};
|
|
|
|
if (old_reg)
|
|
diag_snapshot_reg(&event.mod.old, old_reg);
|
|
if (new_reg)
|
|
diag_snapshot_reg(&event.mod.new, new_reg);
|
|
if (origin) {
|
|
event.mod.origin = *origin;
|
|
event.mod.origin_valid = true;
|
|
} else if (diag_mod_insn_origin(env, insn_idx, &target, &event.mod.origin)) {
|
|
event.mod.origin_valid = true;
|
|
}
|
|
if (old_reg && new_reg &&
|
|
(reason == BPF_DIAG_MOD_WRITE || reason == BPF_DIAG_MOD_SPILL) &&
|
|
!memcmp(&event.mod.old, &event.mod.new, sizeof(event.mod.old)) &&
|
|
!event.mod.origin_valid &&
|
|
diag_mod_keeps_lineage(env, &event))
|
|
return;
|
|
|
|
diag_append_history(env, &event);
|
|
}
|
|
|
|
static struct bpf_reg_state *target_to_reg(struct bpf_verifier_env *env,
|
|
const struct bpf_diag_mod_target *target)
|
|
{
|
|
struct bpf_verifier_state *vstate = env->cur_state;
|
|
struct bpf_func_state *state;
|
|
|
|
state = target->frameno <= vstate->curframe ? vstate->frame[target->frameno] : NULL;
|
|
|
|
if (!state)
|
|
return NULL;
|
|
if (state->diag_frame_id != target->frame_id)
|
|
return NULL;
|
|
|
|
switch (target->kind) {
|
|
case BPF_DIAG_MOD_TARGET_REG:
|
|
if (target->regno >= MAX_BPF_REG)
|
|
return NULL;
|
|
return &state->regs[target->regno];
|
|
case BPF_DIAG_MOD_TARGET_STACK_ARG:
|
|
if (target->stack_arg >= state->out_stack_arg_cnt)
|
|
return NULL;
|
|
return &state->stack_arg_regs[target->stack_arg];
|
|
case BPF_DIAG_MOD_TARGET_STACK_SLOT:
|
|
if (target->spi >= state->allocated_stack / BPF_REG_SIZE)
|
|
return NULL;
|
|
return &state->stack[target->spi].spilled_ptr;
|
|
default:
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
static bool reg_to_target(struct bpf_verifier_env *env, const struct bpf_reg_state *reg,
|
|
struct bpf_diag_mod_target *target)
|
|
{
|
|
struct bpf_verifier_state *vstate = env->cur_state;
|
|
unsigned long addr = (unsigned long)reg;
|
|
int frame;
|
|
|
|
for (frame = 0; frame <= vstate->curframe; frame++) {
|
|
struct bpf_func_state *state = vstate->frame[frame];
|
|
unsigned long start, end;
|
|
u32 nslots = state->allocated_stack / BPF_REG_SIZE;
|
|
int spi;
|
|
|
|
start = (unsigned long)state->regs;
|
|
end = (unsigned long)(state->regs + MAX_BPF_REG);
|
|
if (addr >= start && addr < end) {
|
|
*target = diag_reg_target(state->diag_frame_id, state->frameno,
|
|
reg - state->regs);
|
|
return true;
|
|
}
|
|
|
|
start = (unsigned long)state->stack_arg_regs;
|
|
end = (unsigned long)(state->stack_arg_regs + state->out_stack_arg_cnt);
|
|
if (state->out_stack_arg_cnt && addr >= start && addr < end) {
|
|
*target = diag_stack_arg_target(state->diag_frame_id, state->frameno,
|
|
reg - state->stack_arg_regs);
|
|
return true;
|
|
}
|
|
|
|
start = (unsigned long)state->stack;
|
|
end = (unsigned long)(state->stack + nslots);
|
|
if (nslots && addr >= start && addr < end) {
|
|
spi = ((const char *)reg - (const char *)state->stack) /
|
|
sizeof(*state->stack);
|
|
*target = diag_stack_slot_target(state->diag_frame_id, state->frameno, spi);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void bpf_diag_mod_begin(struct bpf_verifier_env *env, const struct bpf_reg_state *reg,
|
|
const struct bpf_reg_state *origin, enum bpf_diag_mod_reason reason)
|
|
{
|
|
struct bpf_diag *diag = env->diag;
|
|
|
|
if (!diag)
|
|
return;
|
|
diag->mod.active = reg_to_target(env, reg, &diag->mod.target);
|
|
if (!diag->mod.active)
|
|
return;
|
|
diag->mod.target_reg_snapshot = *reg;
|
|
diag->mod.insn_idx = env->insn_idx;
|
|
diag->mod.reason = reason;
|
|
diag->mod.origin_valid = origin && reg_to_target(env, origin, &diag->mod.origin);
|
|
}
|
|
|
|
void bpf_diag_mod_end(struct bpf_verifier_env *env)
|
|
{
|
|
struct bpf_diag *diag = env->diag;
|
|
const struct bpf_reg_state *new_reg;
|
|
|
|
if (!diag || !diag->mod.active)
|
|
return;
|
|
diag->mod.active = false;
|
|
/*
|
|
* Resolve the target again because the enclosing function state's stack
|
|
* may have been reallocated while the modification was in progress.
|
|
*/
|
|
new_reg = target_to_reg(env, &diag->mod.target);
|
|
if (!new_reg)
|
|
return;
|
|
diag_record_mod(env, diag->mod.insn_idx, diag->mod.target, diag->mod.reason,
|
|
&diag->mod.target_reg_snapshot, new_reg,
|
|
diag->mod.origin_valid ? &diag->mod.origin : NULL);
|
|
}
|
|
|
|
void bpf_diag_record_scrub(struct bpf_verifier_env *env, const struct bpf_reg_state *reg,
|
|
enum bpf_diag_mod_reason reason)
|
|
{
|
|
struct bpf_diag_mod_target target;
|
|
|
|
if (!env->diag || reg->type == NOT_INIT || !reg_to_target(env, reg, &target))
|
|
return;
|
|
diag_record_mod(env, env->insn_idx, target, reason, reg, NULL, NULL);
|
|
}
|
|
|
|
void bpf_diag_record_scrub_stack(struct bpf_verifier_env *env,
|
|
const struct bpf_func_state *state, s16 min_off, s16 max_off,
|
|
enum bpf_diag_mod_reason reason)
|
|
{
|
|
diag_record_mod(env, env->insn_idx,
|
|
diag_stack_range_target(state->diag_frame_id, state->frameno, min_off, max_off),
|
|
reason, NULL, NULL, NULL);
|
|
}
|
|
|
|
static void diag_record_ref(struct bpf_verifier_env *env, u32 insn_idx, u8 kind, u32 ref_id)
|
|
{
|
|
struct bpf_diag_history_event event = {
|
|
.insn_idx = insn_idx,
|
|
.kind = kind,
|
|
.ref = {
|
|
.ref_id = ref_id,
|
|
},
|
|
};
|
|
|
|
diag_append_history(env, &event);
|
|
}
|
|
|
|
void bpf_diag_record_ref_acquire(struct bpf_verifier_env *env, u32 insn_idx, u32 ref_id)
|
|
{
|
|
diag_record_ref(env, insn_idx, BPF_DIAG_HISTORY_REF_ACQUIRE, ref_id);
|
|
}
|
|
|
|
void bpf_diag_record_ref_release(struct bpf_verifier_env *env, u32 insn_idx, u32 ref_id)
|
|
{
|
|
diag_record_ref(env, insn_idx, BPF_DIAG_HISTORY_REF_RELEASE, ref_id);
|
|
}
|
|
|
|
void bpf_diag_record_context(struct bpf_verifier_env *env, u32 insn_idx,
|
|
enum bpf_diag_context_kind ctx_kind, bool enter, u32 depth)
|
|
{
|
|
/*
|
|
* Keep leave events so context rendering can stop at a depth-zero exit
|
|
* and show nested-region depth accurately for the active path.
|
|
*/
|
|
struct bpf_diag_history_event event = {
|
|
.insn_idx = insn_idx,
|
|
.kind = BPF_DIAG_HISTORY_CONTEXT,
|
|
.ctx = {
|
|
.kind = ctx_kind,
|
|
.enter = enter,
|
|
.depth = depth,
|
|
},
|
|
};
|
|
|
|
diag_append_history(env, &event);
|
|
}
|
|
|
|
static int diag_history_context_start_idx(const struct bpf_diag_log *log,
|
|
const struct bpf_diag_history_opts *opts)
|
|
{
|
|
int i;
|
|
|
|
if (!opts->ctx_depth)
|
|
return 0;
|
|
|
|
/* Find the most recent outermost entry, or a depth-zero exit. */
|
|
for (i = log->cnt; i > 0; i--) {
|
|
const struct bpf_diag_history_event *event;
|
|
|
|
event = &log->events[log_pos(log, i - 1)];
|
|
|
|
if (event->kind != BPF_DIAG_HISTORY_CONTEXT || event->ctx.kind != opts->ctx_kind)
|
|
continue;
|
|
|
|
if (event->ctx.enter && event->ctx.depth == 1)
|
|
return i - 1;
|
|
if (!event->ctx.enter && event->ctx.depth == 0)
|
|
return 0;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
struct bpf_diag_history_filter {
|
|
const struct bpf_diag_history_opts *opts;
|
|
u32 lineage_start;
|
|
bool lineage_valid;
|
|
};
|
|
|
|
static bool diag_target_matches(const struct bpf_diag_mod_target *event_target,
|
|
const struct bpf_diag_mod_target *target)
|
|
{
|
|
int slot_off;
|
|
|
|
if (event_target->frame_id != target->frame_id || event_target->frameno != target->frameno)
|
|
return false;
|
|
|
|
if (event_target->kind == BPF_DIAG_MOD_TARGET_STACK_RANGE &&
|
|
target->kind == BPF_DIAG_MOD_TARGET_STACK_SLOT) {
|
|
slot_off = -(target->spi + 1) * BPF_REG_SIZE;
|
|
return event_target->range.min_off < slot_off + BPF_REG_SIZE &&
|
|
event_target->range.max_off > slot_off;
|
|
}
|
|
|
|
if (event_target->kind != target->kind)
|
|
return false;
|
|
|
|
switch (target->kind) {
|
|
case BPF_DIAG_MOD_TARGET_REG:
|
|
return event_target->regno == target->regno;
|
|
case BPF_DIAG_MOD_TARGET_STACK_ARG:
|
|
return event_target->stack_arg == target->stack_arg;
|
|
case BPF_DIAG_MOD_TARGET_STACK_SLOT:
|
|
return event_target->spi == target->spi;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static void diag_build_lineage(struct bpf_verifier_env *env, struct bpf_diag_log *log,
|
|
struct bpf_diag_history_filter *filter)
|
|
{
|
|
const struct bpf_diag_history_opts *opts = filter->opts;
|
|
struct bpf_diag_mod_target target;
|
|
int i;
|
|
|
|
for (i = 0; i < log->cnt; i++)
|
|
log->events[log_pos(log, i)].in_lineage = false;
|
|
|
|
if (opts->scope == BPF_DIAG_HISTORY_SCOPE_REG)
|
|
target = diag_reg_target(opts->frame_id, opts->frameno, opts->regno);
|
|
else if (opts->scope == BPF_DIAG_HISTORY_SCOPE_STACK_ARG)
|
|
target = diag_stack_arg_target(opts->frame_id, opts->frameno,
|
|
opts->stack_arg_slot);
|
|
else
|
|
return;
|
|
|
|
/*
|
|
* Find the nearest mutation of the active target. A fill or spill changes
|
|
* the target to its origin, so the same walk follows register/stack
|
|
* lineage recursively until it reaches the write that created the value.
|
|
*/
|
|
for (i = log->cnt; i > 0; i--) {
|
|
struct bpf_diag_history_event *event;
|
|
|
|
event = &log->events[log_pos(log, i - 1)];
|
|
if (event->kind != BPF_DIAG_HISTORY_MOD ||
|
|
!diag_target_matches(&event->mod.target, &target))
|
|
continue;
|
|
|
|
event->in_lineage = true;
|
|
filter->lineage_start = i - 1;
|
|
filter->lineage_valid = true;
|
|
|
|
if (event->mod.origin_valid) {
|
|
target = event->mod.origin;
|
|
continue;
|
|
}
|
|
if (event->mod.reason != BPF_DIAG_MOD_WRITE &&
|
|
event->mod.reason != BPF_DIAG_MOD_SPILL)
|
|
continue;
|
|
if (diag_mod_keeps_lineage(env, event))
|
|
continue;
|
|
break;
|
|
}
|
|
}
|
|
|
|
static int diag_history_start_idx(const struct bpf_diag_log *log,
|
|
const struct bpf_diag_history_filter *filter)
|
|
{
|
|
const struct bpf_diag_history_opts *opts = filter->opts;
|
|
int i;
|
|
|
|
if (opts->scope == BPF_DIAG_HISTORY_SCOPE_CONTEXT)
|
|
return diag_history_context_start_idx(log, opts);
|
|
if (filter->lineage_valid)
|
|
return filter->lineage_start;
|
|
if (opts->scope != BPF_DIAG_HISTORY_SCOPE_REF)
|
|
return 0;
|
|
|
|
for (i = log->cnt; i > 0; i--) {
|
|
const struct bpf_diag_history_event *event;
|
|
|
|
event = &log->events[log_pos(log, i - 1)];
|
|
if (event->kind == BPF_DIAG_HISTORY_REF_ACQUIRE &&
|
|
event->ref.ref_id == opts->ref_id)
|
|
return i - 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static bool diag_history_event_visible(const struct bpf_diag_history_event *event,
|
|
const struct bpf_diag_history_filter *filter)
|
|
{
|
|
const struct bpf_diag_history_opts *opts = filter->opts;
|
|
|
|
switch (event->kind) {
|
|
case BPF_DIAG_HISTORY_BRANCH:
|
|
return true;
|
|
case BPF_DIAG_HISTORY_MOD:
|
|
return filter->lineage_valid && event->in_lineage;
|
|
case BPF_DIAG_HISTORY_REF_ACQUIRE:
|
|
case BPF_DIAG_HISTORY_REF_RELEASE:
|
|
return opts->scope == BPF_DIAG_HISTORY_SCOPE_REF &&
|
|
event->ref.ref_id == opts->ref_id;
|
|
case BPF_DIAG_HISTORY_CONTEXT:
|
|
return opts->scope == BPF_DIAG_HISTORY_SCOPE_CONTEXT &&
|
|
event->ctx.kind == opts->ctx_kind;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static const char *diag_s64_bound_name(s64 value)
|
|
{
|
|
if (value == S64_MIN)
|
|
return "S64_MIN";
|
|
if (value == S64_MAX)
|
|
return "S64_MAX";
|
|
return NULL;
|
|
}
|
|
|
|
static const char *diag_u64_bound_name(u64 value)
|
|
{
|
|
if (value == U64_MAX)
|
|
return "U64_MAX";
|
|
return NULL;
|
|
}
|
|
|
|
static const char *diag_s64_str(struct bpf_verifier_env *env, s64 value)
|
|
{
|
|
return diag_s64_bound_name(value) ?: bpf_diag_fmt(env, "%lld", value);
|
|
}
|
|
|
|
static const char *diag_u64_str(struct bpf_verifier_env *env, u64 value)
|
|
{
|
|
return diag_u64_bound_name(value) ?: bpf_diag_fmt(env, "%llu", value);
|
|
}
|
|
|
|
static bool diag_cnum64_unknown(struct cnum64 range)
|
|
{
|
|
return cnum64_smin(range) == S64_MIN && cnum64_smax(range) == S64_MAX &&
|
|
cnum64_umin(range) == 0 && cnum64_umax(range) == U64_MAX;
|
|
}
|
|
|
|
static bool diag_snapshot_unknown(const struct bpf_diag_reg_snapshot *snapshot)
|
|
{
|
|
return tnum_is_unknown(snapshot->var_off) && diag_cnum64_unknown(snapshot->r64);
|
|
}
|
|
|
|
static const char *diag_scalar_range(struct bpf_verifier_env *env, struct cnum64 range)
|
|
{
|
|
return bpf_diag_fmt(env, "signed range [%s, %s], unsigned range [%s, %s]",
|
|
diag_s64_str(env, cnum64_smin(range)),
|
|
diag_s64_str(env, cnum64_smax(range)),
|
|
diag_u64_str(env, cnum64_umin(range)),
|
|
diag_u64_str(env, cnum64_umax(range)));
|
|
}
|
|
|
|
const char *bpf_diag_fmt_s64_sum(struct bpf_verifier_env *env, s64 value, int addend)
|
|
{
|
|
s64 sum;
|
|
|
|
if (check_add_overflow(value, (s64)addend, &sum))
|
|
return bpf_diag_fmt(env, "%lld plus %d (%s)", value, addend,
|
|
addend < 0 ? "below S64_MIN" : "above S64_MAX");
|
|
|
|
return bpf_diag_fmt(env, "%lld", sum);
|
|
}
|
|
|
|
static const char *diag_access_offset(struct bpf_verifier_env *env, int off,
|
|
const struct bpf_reg_state *reg)
|
|
{
|
|
if (tnum_is_const(reg->var_off))
|
|
return bpf_diag_fmt(env, "constant %s",
|
|
bpf_diag_fmt_s64_sum(env, (s64)reg->var_off.value, off));
|
|
|
|
if (tnum_is_unknown(reg->var_off) && diag_cnum64_unknown(reg->r64))
|
|
return bpf_diag_fmt(env, "unbounded");
|
|
|
|
if (off)
|
|
return bpf_diag_fmt(env,
|
|
"variable: known bits %#llx, unknown mask %#llx, plus fixed offset %d; %s",
|
|
(u64)reg->var_off.value, reg->var_off.mask, off,
|
|
diag_scalar_range(env, reg->r64));
|
|
return bpf_diag_fmt(env, "variable: known bits %#llx, unknown mask %#llx; %s",
|
|
(u64)reg->var_off.value, reg->var_off.mask,
|
|
diag_scalar_range(env, reg->r64));
|
|
}
|
|
|
|
void bpf_diag_mem_bounds(struct bpf_verifier_env *env, u32 insn_idx, int regno,
|
|
const char *reg_name, const char *type_name, const char *proof,
|
|
int off, int size, u32 mem_size, const struct bpf_reg_state *reg)
|
|
{
|
|
const struct bpf_func_state *frame = diag_current_frame(env);
|
|
struct bpf_diag_history_opts opts = {
|
|
.scope = BPF_DIAG_HISTORY_SCOPE_REG,
|
|
.frame_id = frame->diag_frame_id,
|
|
.frameno = frame->frameno,
|
|
.regno = regno,
|
|
};
|
|
const char *offset_desc;
|
|
|
|
if (!bpf_diag_enabled(env))
|
|
return;
|
|
|
|
offset_desc = diag_access_offset(env, off, reg);
|
|
|
|
bpf_diag_header(env, MEMORY_SAFETY, "access outside bounds");
|
|
diag_reason(
|
|
env, "The verifier cannot prove offset + access_size <= object_size. Here, %s. %s is %s; offset is %s; access_size is %d; object_size is %u.",
|
|
proof, reg_name, type_name, offset_desc, size, mem_size);
|
|
|
|
diag_section(env, "At");
|
|
bpf_diag_source(env, insn_idx, "error", "access may be outside object bounds");
|
|
|
|
if (regno >= 0)
|
|
diag_print_history(env, &opts);
|
|
|
|
diag_suggestion(
|
|
env, "Add or adjust a bounds check that proves offset + access_size stays within the object.");
|
|
}
|
|
|
|
static const char *diag_lock_name(const struct bpf_reference_state *lock)
|
|
{
|
|
switch (lock->type) {
|
|
case REF_TYPE_LOCK:
|
|
return "bpf_spin_lock";
|
|
case REF_TYPE_RES_LOCK:
|
|
return "resource spin lock";
|
|
case REF_TYPE_RES_LOCK_IRQ:
|
|
return "IRQ-saving resource spin lock";
|
|
default:
|
|
return "lock";
|
|
}
|
|
}
|
|
|
|
static void diag_res_report(struct bpf_verifier_env *env, u32 insn_idx, const char *problem,
|
|
const char *reason)
|
|
{
|
|
bpf_diag_header(env, RESOURCE_LIFETIME_SAFETY, problem);
|
|
diag_reason(env, "%s", reason);
|
|
|
|
diag_section(env, "At");
|
|
bpf_diag_source(env, insn_idx, "error", "%s", problem);
|
|
}
|
|
|
|
void bpf_diag_res(struct bpf_verifier_env *env, u32 insn_idx, const char *problem,
|
|
const char *reason, const char *suggestion)
|
|
{
|
|
diag_res_report(env, insn_idx, problem, reason);
|
|
diag_suggestion(env, "%s", suggestion);
|
|
}
|
|
|
|
void bpf_diag_lock(struct bpf_verifier_env *env, u32 insn_idx, const char *problem,
|
|
const char *reason, const char *suggestion,
|
|
const struct bpf_reference_state *active_lock)
|
|
{
|
|
diag_res_report(env, insn_idx, problem, reason);
|
|
|
|
if (active_lock) {
|
|
diag_section(env, "Active lock");
|
|
bpf_diag_source(env, active_lock->insn_idx, "acquired",
|
|
"active %s has verifier identity %d",
|
|
diag_lock_name(active_lock), active_lock->id);
|
|
}
|
|
|
|
diag_suggestion(env, "%s", suggestion);
|
|
}
|
|
|
|
void bpf_diag_irq(struct bpf_verifier_env *env, u32 insn_idx, const char *problem,
|
|
const char *reason, const char *suggestion, u32 depth)
|
|
{
|
|
struct bpf_diag_history_opts opts = {
|
|
.scope = BPF_DIAG_HISTORY_SCOPE_CONTEXT,
|
|
.ctx_kind = BPF_DIAG_CONTEXT_IRQ,
|
|
.ctx_depth = depth,
|
|
};
|
|
|
|
bpf_diag_header(env, RESOURCE_LIFETIME_SAFETY, problem);
|
|
diag_reason(env, "%s", reason);
|
|
|
|
diag_section(env, "At");
|
|
bpf_diag_source(env, insn_idx, "error", "%s", problem);
|
|
|
|
if (depth)
|
|
diag_print_history(env, &opts);
|
|
|
|
diag_suggestion(env, "%s", suggestion);
|
|
}
|
|
|
|
void bpf_diag_leak(struct bpf_verifier_env *env, u32 ref_id, u32 alloc_insn, u32 fail_insn)
|
|
{
|
|
struct bpf_diag_history_opts opts = {
|
|
.scope = BPF_DIAG_HISTORY_SCOPE_REF,
|
|
.ref_id = ref_id,
|
|
};
|
|
|
|
bpf_diag_header(env, RESOURCE_LIFETIME_SAFETY, "unreleased resource");
|
|
diag_reason(
|
|
env, "Owned resource (id=%u) was acquired at instruction %u and still needs to be released before this exit path.",
|
|
ref_id, alloc_insn);
|
|
|
|
diag_section(env, "At");
|
|
bpf_diag_source(env, fail_insn, "error",
|
|
"owned resource (id=%u) still needs release", ref_id);
|
|
|
|
diag_print_history(env, &opts);
|
|
|
|
diag_suggestion(
|
|
env, "Release or transfer ownership of the acquired resource on every path before the program exits.");
|
|
}
|
|
|
|
static const char *diag_var_offset(struct bpf_verifier_env *env,
|
|
const struct bpf_diag_reg_snapshot *snapshot)
|
|
{
|
|
if (tnum_is_const(snapshot->var_off))
|
|
return bpf_diag_fmt(env, "at offset %lld", (s64)snapshot->var_off.value);
|
|
|
|
if (diag_snapshot_unknown(snapshot))
|
|
return bpf_diag_fmt(env, "with unknown offset");
|
|
|
|
return bpf_diag_fmt(env,
|
|
"with variable offset: known bits %#llx, unknown mask %#llx, %s",
|
|
snapshot->var_off.value, snapshot->var_off.mask,
|
|
diag_scalar_range(env, snapshot->r64));
|
|
}
|
|
|
|
static const char *diag_reg_map_name(const struct bpf_map *map)
|
|
{
|
|
if (!map || !map->name[0])
|
|
return NULL;
|
|
|
|
return map->name;
|
|
}
|
|
|
|
static const char *diag_reg_snapshot(struct bpf_verifier_env *env,
|
|
const struct bpf_diag_reg_snapshot *snapshot)
|
|
{
|
|
const char *type_name = reg_type_str(env, snapshot->type);
|
|
const char *offset = diag_var_offset(env, snapshot);
|
|
const char *btf = snapshot->btf && snapshot->btf_id ?
|
|
bpf_diag_fmt_btf_type(env, snapshot->btf, snapshot->btf_id) : NULL;
|
|
const char *map_name;
|
|
|
|
if (snapshot->type == SCALAR_VALUE) {
|
|
if (tnum_is_const(snapshot->var_off))
|
|
return bpf_diag_fmt(env, "integer scalar value %lld",
|
|
(s64)snapshot->var_off.value);
|
|
if (diag_snapshot_unknown(snapshot))
|
|
return bpf_diag_fmt(env, "integer scalar with unknown value");
|
|
if (cnum64_is_const(snapshot->r64))
|
|
return bpf_diag_fmt(env, "integer scalar value %lld",
|
|
cnum64_smin(snapshot->r64));
|
|
return bpf_diag_fmt(env, "integer scalar with %s",
|
|
diag_scalar_range(env, snapshot->r64));
|
|
}
|
|
|
|
if (snapshot->type == NOT_INIT)
|
|
return bpf_diag_fmt(env, "uninitialized value");
|
|
|
|
if (base_type(snapshot->type) == PTR_TO_CTX)
|
|
return bpf_diag_fmt(env, "context pointer %s", offset);
|
|
|
|
if (base_type(snapshot->type) == PTR_TO_STACK)
|
|
return bpf_diag_fmt(env, "stack pointer %s", offset);
|
|
|
|
if (base_type(snapshot->type) == PTR_TO_MAP_VALUE) {
|
|
const char *kind = type_may_be_null(snapshot->type) ? "nullable map value" :
|
|
"map value";
|
|
|
|
map_name = diag_reg_map_name(snapshot->map_ptr);
|
|
if (map_name)
|
|
return bpf_diag_fmt(env, "%s from %s %s", kind, map_name, offset);
|
|
return bpf_diag_fmt(env, "%s %s", kind, offset);
|
|
}
|
|
|
|
if (base_type(snapshot->type) == CONST_PTR_TO_MAP) {
|
|
map_name = diag_reg_map_name(snapshot->map_ptr);
|
|
if (map_name)
|
|
return bpf_diag_fmt(env, "map pointer for map %s", map_name);
|
|
return bpf_diag_fmt(env, "map pointer");
|
|
}
|
|
|
|
if (type_is_non_owning_ref(snapshot->type)) {
|
|
if (btf)
|
|
return bpf_diag_fmt(env, "borrowed allocated object pointer type=%s", btf);
|
|
return bpf_diag_fmt(env, "borrowed allocated object pointer");
|
|
}
|
|
|
|
if (type_is_ptr_alloc_obj(snapshot->type)) {
|
|
if (btf)
|
|
return bpf_diag_fmt(env, "owned allocated object pointer type=%s", btf);
|
|
return bpf_diag_fmt(env, "owned allocated object pointer");
|
|
}
|
|
|
|
if (base_type(snapshot->type) == PTR_TO_BTF_ID && btf)
|
|
return bpf_diag_fmt(env, "%s type=%s %s", type_name, btf, offset);
|
|
|
|
return bpf_diag_fmt(env, "%s %s", type_name, offset);
|
|
}
|
|
|
|
static const char *diag_mod_target_desc(struct bpf_verifier_env *env,
|
|
const struct bpf_diag_mod_target *target)
|
|
{
|
|
switch (target->kind) {
|
|
case BPF_DIAG_MOD_TARGET_REG:
|
|
return bpf_diag_fmt(env, "R%u", target->regno);
|
|
case BPF_DIAG_MOD_TARGET_STACK_ARG:
|
|
return bpf_diag_fmt(env, "*(R11-%u)", (target->stack_arg + 1) * BPF_REG_SIZE);
|
|
case BPF_DIAG_MOD_TARGET_STACK_SLOT:
|
|
return bpf_diag_fmt(env, "stack slot fp%d", -(target->spi + 1) * BPF_REG_SIZE);
|
|
default:
|
|
return "value";
|
|
}
|
|
}
|
|
|
|
static void diag_print_mod(struct bpf_verifier_env *env, const struct bpf_diag_history_event *event)
|
|
{
|
|
const struct bpf_diag_mod_target *target = &event->mod.target;
|
|
const char *target_desc, *reason = NULL, *old, *new;
|
|
const char *label = "update";
|
|
|
|
if (target->kind == BPF_DIAG_MOD_TARGET_STACK_RANGE) {
|
|
bpf_diag_source(
|
|
env, event->insn_idx, "invalidated",
|
|
"variable-offset stack write may affect bytes fp%d through fp%d",
|
|
target->range.min_off, target->range.max_off - 1);
|
|
return;
|
|
}
|
|
|
|
old = diag_reg_snapshot(env, &event->mod.old);
|
|
new = diag_reg_snapshot(env, &event->mod.new);
|
|
target_desc = diag_mod_target_desc(env, target);
|
|
|
|
switch (event->mod.reason) {
|
|
case BPF_DIAG_MOD_REF_RELEASE:
|
|
reason = target->kind == BPF_DIAG_MOD_TARGET_REG ? "resource release invalidated "
|
|
"this pointer" :
|
|
"resource release invalidated "
|
|
"this value";
|
|
break;
|
|
case BPF_DIAG_MOD_PKT_DATA_CHANGE:
|
|
reason = "packet data may have moved";
|
|
break;
|
|
case BPF_DIAG_MOD_NON_OWN_REF:
|
|
reason = "leaving the protected region invalidated this borrowed pointer";
|
|
break;
|
|
case BPF_DIAG_MOD_CALLER_SAVED:
|
|
reason = target->kind == BPF_DIAG_MOD_TARGET_STACK_ARG ?
|
|
"call invalidated this outgoing stack argument" :
|
|
"call invalidated this caller-saved register";
|
|
break;
|
|
case BPF_DIAG_MOD_WRITE:
|
|
if (target->kind == BPF_DIAG_MOD_TARGET_STACK_SLOT)
|
|
reason = "a later stack write overwrote this spilled value";
|
|
break;
|
|
case BPF_DIAG_MOD_SPILL:
|
|
label = "spilled";
|
|
break;
|
|
case BPF_DIAG_MOD_VAR_WRITE:
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (reason) {
|
|
bpf_diag_source(env, event->insn_idx, "invalidated",
|
|
"%s: %s; previous value was %s", target_desc, reason, old);
|
|
return;
|
|
}
|
|
|
|
bpf_diag_source(env, event->insn_idx, label, "%s changed from %s to %s", target_desc,
|
|
old, new);
|
|
}
|
|
|
|
static void diag_print_ref_event(struct bpf_verifier_env *env,
|
|
const struct bpf_diag_history_event *event)
|
|
{
|
|
const char *label;
|
|
|
|
label = event->kind == BPF_DIAG_HISTORY_REF_ACQUIRE ? "acquired" : "released";
|
|
bpf_diag_source(env, event->insn_idx, label, "owned resource (id=%u)",
|
|
event->ref.ref_id);
|
|
}
|
|
|
|
static const char *diag_context_name(enum bpf_diag_context_kind kind)
|
|
{
|
|
switch (kind) {
|
|
case BPF_DIAG_CONTEXT_RCU:
|
|
return "RCU read lock region";
|
|
case BPF_DIAG_CONTEXT_PREEMPT:
|
|
return "non-preemptible region";
|
|
case BPF_DIAG_CONTEXT_IRQ:
|
|
return "IRQ-disabled region";
|
|
case BPF_DIAG_CONTEXT_LOCK:
|
|
return "lock region";
|
|
case BPF_DIAG_CONTEXT_NONE:
|
|
default:
|
|
return "non-sleepable program";
|
|
}
|
|
}
|
|
|
|
static void diag_print_context_event(struct bpf_verifier_env *env,
|
|
const struct bpf_diag_history_event *event)
|
|
{
|
|
bpf_diag_source(env, event->insn_idx, "context", "%s %s; depth is now %u",
|
|
event->ctx.enter ? "entered" : "left",
|
|
diag_context_name(event->ctx.kind), event->ctx.depth);
|
|
}
|
|
|
|
static void diag_print_history(struct bpf_verifier_env *env,
|
|
const struct bpf_diag_history_opts *opts)
|
|
{
|
|
const struct bpf_diag_history_event *event;
|
|
struct bpf_diag_history_filter filter = {
|
|
.opts = opts,
|
|
};
|
|
struct bpf_diag_log *log;
|
|
struct diag_fmt_mark mark;
|
|
bool first = true;
|
|
int start_idx;
|
|
u32 i, visible_cnt = 0, visible_idx = 0;
|
|
|
|
if (!bpf_diag_enabled(env))
|
|
return;
|
|
|
|
if (!env->diag)
|
|
return;
|
|
log = &env->diag->log;
|
|
|
|
diag_build_lineage(env, log, &filter);
|
|
|
|
start_idx = diag_history_start_idx(log, &filter);
|
|
for (i = start_idx; i < log->cnt; i++) {
|
|
event = &log->events[log_pos(log, i)];
|
|
if (diag_history_event_visible(event, &filter))
|
|
visible_cnt++;
|
|
}
|
|
|
|
if (!visible_cnt && !log->first_seq && opts->scope == BPF_DIAG_HISTORY_SCOPE_STACK_ARG)
|
|
return;
|
|
|
|
diag_section(env, "Causal path");
|
|
mark = diag_fmt_save(env);
|
|
for (i = start_idx; i < log->cnt; i++) {
|
|
event = &log->events[log_pos(log, i)];
|
|
if (!diag_history_event_visible(event, &filter))
|
|
continue;
|
|
|
|
diag_fmt_restore(env, mark);
|
|
if (visible_cnt > BPF_DIAG_HISTORY_RENDER_MAX &&
|
|
visible_idx >= BPF_DIAG_HISTORY_RENDER_MAX / 2 &&
|
|
visible_idx < visible_cnt - BPF_DIAG_HISTORY_RENDER_MAX / 2) {
|
|
if (visible_idx++ != BPF_DIAG_HISTORY_RENDER_MAX / 2)
|
|
continue;
|
|
if (!first)
|
|
diag_write(env, "\n");
|
|
first = false;
|
|
diag_write(env, " %u intermediate causal-history events omitted\n",
|
|
visible_cnt - BPF_DIAG_HISTORY_RENDER_MAX);
|
|
continue;
|
|
}
|
|
visible_idx++;
|
|
|
|
if (!first)
|
|
diag_write(env, "\n");
|
|
first = false;
|
|
|
|
switch (event->kind) {
|
|
case BPF_DIAG_HISTORY_BRANCH:
|
|
bpf_diag_source(env, event->insn_idx, "branch",
|
|
"took the %s branch of this conditional, goto %s",
|
|
event->branch.cond_true ? "true" : "false",
|
|
event->branch.cond_true ? "followed" : "not followed");
|
|
break;
|
|
case BPF_DIAG_HISTORY_MOD:
|
|
diag_print_mod(env, event);
|
|
break;
|
|
case BPF_DIAG_HISTORY_REF_ACQUIRE:
|
|
case BPF_DIAG_HISTORY_REF_RELEASE:
|
|
diag_print_ref_event(env, event);
|
|
break;
|
|
case BPF_DIAG_HISTORY_CONTEXT:
|
|
diag_print_context_event(env, event);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!visible_cnt)
|
|
diag_write(env, " no retained diagnostic events on this path\n");
|
|
if (log->first_seq)
|
|
diag_write(env, " %llu older causal-history event%s not retained because diagnostic "
|
|
"event storage reached capacity\n",
|
|
log->first_seq, log->first_seq == 1 ? "" : "s");
|
|
diag_fmt_restore(env, mark);
|
|
}
|