[PATCH] tracing: hist: free var refs regardless of how often they are referenced

From: Donggeun Yoo

Date: Sun Sep 06 2026 - 08:40:44 EST


Using the same variable three or more times in one hist trigger leaks the
variable reference and its strings when the trigger is removed.

commit 656fe2ba85e8 ("tracing: Use hist trigger's var_ref array to destroy
var_refs") made a trigger's var_refs[] array the only owner of a var ref:
destroy_hist_field() returns early for HIST_FIELD_FL_VAR_REF, so the field
expressions never destroy one. One entry, freed once, no count needed.

commit 8bcebc77e85f ("tracing: Fix histogram code when expression has same
var as value") then made repeated references share one object and added a
count of them. Only the increment side exists, since those expressions
still return early and never drop a reference, so __destroy_hist_field()
sees how many references were created rather than how many are left. It
frees when the decremented count is 0 or 1, so two references work and
three or more leak.

Sharing kept one array entry per object, and create_var_ref() searches and
appends within a single trigger, so nothing outside it holds the object.
Removing a trigger whose variables are still referenced is already refused
by check_var_refs() with -EBUSY. Drop the count and free unconditionally.

Fixes: 8bcebc77e85f ("tracing: Fix histogram code when expression has same var as value")
Signed-off-by: Donggeun Yoo <donggeunyoo.kernel@xxxxxxxxx>
---
Reproduced under QEMU (x86_64) with CONFIG_DEBUG_KMEMLEAK. Two triggers
differing only in a third reference to the same variable, each installed
and removed 200 times:

hist:keys=next_pid:delta=common_timestamp-$start,start2=$start:
onmatch(sched.sched_waking).trace(first,$start2,common_timestamp,next_pid,$delta)

... plus delta2=common_timestamp-$start

two references 0 unreferenced objects, 0 bytes
three references 620 / 666 objects, 62000 / 66600 bytes over two runs

With this patch both are 0. kmemleak points at the var ref itself and at
the strings init_var_ref() attaches to it:

unreferenced object (size 192):
create_hist_field+0x39/0x390
create_var_ref+0x96/0x100
parse_atom+0x4ad/0x910
unreferenced object (size 8):
hex dump: 73 74 61 72 74 00 00 00 start...
kstrdup+0x37/0x70
init_var_ref+0x88/0x110

tools/testing/selftests/ftrace test.d/trigger: 45 tests, results identical
before and after, including the ones covering variable references --
field variable support, fully-qualified variable reference support,
inter-event combined, onmatch, onmax, onmatch-onmax and trace action all
pass. Three tests fail identically with and without the patch (onchange
action, and trace action with a dynamic string param); I did not chase
those down.

checkpatch --strict is clean and an x86_64 W=1 build of the file adds no
warnings.

kernel/trace/trace_events_hist.c | 16 +---------------
1 file changed, 1 insertion(+), 15 deletions(-)

diff --git a/kernel/trace/trace_events_hist.c b/kernel/trace/trace_events_hist.c
index 963e0d6b61fd..f90680b33a37 100644
--- a/kernel/trace/trace_events_hist.c
+++ b/kernel/trace/trace_events_hist.c
@@ -169,7 +169,6 @@ struct hist_field {
struct hist_field *operands[HIST_FIELD_OPERANDS_MAX];
struct hist_trigger_data *hist_data;
enum hist_field_fn fn_num;
- unsigned int ref;
unsigned int size;
unsigned int offset;
unsigned int is_signed;
@@ -1913,16 +1912,8 @@ static int contains_operator(char *str, char **sep)
return field_op;
}

-static void get_hist_field(struct hist_field *hist_field)
-{
- hist_field->ref++;
-}
-
static void __destroy_hist_field(struct hist_field *hist_field)
{
- if (--hist_field->ref > 1)
- return;
-
kfree(hist_field->var.name);
kfree(hist_field->name);

@@ -1969,8 +1960,6 @@ static struct hist_field *create_hist_field(struct hist_trigger_data *hist_data,
if (!hist_field)
return NULL;

- hist_field->ref = 1;
-
hist_field->hist_data = hist_data;

if (flags & HIST_FIELD_FL_EXPR || flags & HIST_FIELD_FL_ALIAS)
@@ -2223,10 +2212,8 @@ static struct hist_field *create_var_ref(struct hist_trigger_data *hist_data,
for (i = 0; i < hist_data->n_var_refs; i++) {
ref_field = hist_data->var_refs[i];
if (ref_field->var.idx == var_field->var.idx &&
- ref_field->var.hist_data == var_field->hist_data) {
- get_hist_field(ref_field);
+ ref_field->var.hist_data == var_field->hist_data)
return ref_field;
- }
}
/* Sanity check to avoid out-of-bound write on 'hist_data->var_refs' */
if (hist_data->n_var_refs >= TRACING_MAP_VARS_MAX)
@@ -3276,7 +3263,6 @@ static struct hist_field *create_var(struct hist_trigger_data *hist_data,
goto out;
}

- var->ref = 1;
var->flags = HIST_FIELD_FL_VAR;
var->var.idx = idx;
var->var.hist_data = var->hist_data = hist_data;

base-commit: 1fc5a74b108fc90951890ec513ac81869f5eaff1
--
2.53.0