Skip to content

Commit c3e2167

Browse files
committed
feat: enhance perf_event attachment with detaching field and concurrent handling
1 parent ae9b2ea commit c3e2167

2 files changed

Lines changed: 65 additions & 20 deletions

File tree

src/userspace_codegen.ml

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4053,7 +4053,8 @@ struct attachment_entry {
40534053
uint32_t flags;
40544054
struct bpf_link *link; // For kprobe/tracepoint programs (NULL for XDP)
40554055
int ifindex; // For XDP programs (0 for kprobe/tracepoint)
4056-
int perf_fd; // For perf_event programs (-1 otherwise)
4056+
int perf_fd; // For perf_event programs (-1 otherwise)
4057+
int detaching; // Non-zero while teardown is in progress
40574058
enum bpf_prog_type type;
40584059
struct attachment_entry *next;
40594060
};
@@ -4081,11 +4082,14 @@ static int add_attachment(int prog_fd, const char *target, uint32_t flags,
40814082
entry->perf_fd = perf_fd;
40824083
entry->type = type;
40834084

4085+
entry->detaching = 0;
40844086
pthread_mutex_lock(&attachment_mutex);
4085-
/* Reject duplicate insertions atomically */
4087+
/* Reject duplicate insertions atomically.
4088+
* Skip entries that are currently being torn down (detaching != 0) so that
4089+
* a new attach can succeed while the old detach is still running. */
40864090
struct attachment_entry *existing = attached_programs;
40874091
while (existing) {
4088-
if (existing->prog_fd == prog_fd) {
4092+
if (existing->prog_fd == prog_fd && !existing->detaching) {
40894093
pthread_mutex_unlock(&attachment_mutex);
40904094
free(entry);
40914095
fprintf(stderr, "Program with fd %d is already attached. Use detach() first.\n", prog_fd);
@@ -4328,18 +4332,16 @@ static struct bpf_program *find_prog_by_fd(int prog_fd) {
43284332
return;
43294333
}
43304334

4331-
/* Atomically extract the entry from the list so concurrent detach/perf_read
4332-
* cannot dereference a freed pointer. */
4335+
/* Phase 1: mark the entry as detaching under the lock so concurrent
4336+
* perf_read skips it and a concurrent add_attachment can proceed. */
43334337
pthread_mutex_lock(&attachment_mutex);
4334-
struct attachment_entry *entry = NULL;
4335-
struct attachment_entry **cur = &attached_programs;
4336-
while (*cur) {
4337-
if ((*cur)->prog_fd == prog_fd) {
4338-
entry = *cur;
4339-
*cur = entry->next;
4338+
struct attachment_entry *entry = attached_programs;
4339+
while (entry) {
4340+
if (entry->prog_fd == prog_fd && !entry->detaching) {
4341+
entry->detaching = 1;
43404342
break;
43414343
}
4342-
cur = &(*cur)->next;
4344+
entry = entry->next;
43434345
}
43444346
pthread_mutex_unlock(&attachment_mutex);
43454347

@@ -4400,6 +4402,17 @@ static struct bpf_program *find_prog_by_fd(int prog_fd) {
44004402
break;
44014403
}
44024404

4405+
/* Phase 2: teardown is complete; remove entry from tracking list and free. */
4406+
pthread_mutex_lock(&attachment_mutex);
4407+
struct attachment_entry **cur2 = &attached_programs;
4408+
while (*cur2) {
4409+
if (*cur2 == entry) {
4410+
*cur2 = entry->next;
4411+
break;
4412+
}
4413+
cur2 = &(*cur2)->next;
4414+
}
4415+
pthread_mutex_unlock(&attachment_mutex);
44034416
free(entry);
44044417
}|} detach_perf_case
44054418
else "" in
@@ -4685,15 +4698,18 @@ int64_t ks_read_perf_count(int perf_fd) {
46854698
/* Read the counter for the perf_event program bound to prog_fd.
46864699
* Looks up the perf_fd from the attachment table and calls ks_read_perf_count. */
46874700
int64_t ks_perf_read(int prog_fd) {
4688-
/* Read perf_fd under the lock so the entry cannot be freed concurrently */
4701+
/* Dup perf_fd under the lock so a concurrent detach closing the original fd
4702+
* cannot affect the fd we read from. Skip entries marked detaching. */
46894703
pthread_mutex_lock(&attachment_mutex);
46904704
int found = 0;
4691-
int perf_fd = -1;
4705+
int dup_fd = -1;
46924706
struct attachment_entry *cur = attached_programs;
46934707
while (cur) {
46944708
if (cur->prog_fd == prog_fd) {
4695-
found = 1;
4696-
perf_fd = cur->perf_fd;
4709+
if (!cur->detaching && cur->perf_fd >= 0) {
4710+
found = 1;
4711+
dup_fd = dup(cur->perf_fd);
4712+
}
46974713
break;
46984714
}
46994715
cur = cur->next;
@@ -4703,11 +4719,13 @@ int64_t ks_perf_read(int prog_fd) {
47034719
fprintf(stderr, "ks_perf_read: no active attachment for program fd %d\n", prog_fd);
47044720
return -1;
47054721
}
4706-
if (perf_fd < 0) {
4722+
if (dup_fd < 0) {
47074723
fprintf(stderr, "ks_perf_read: program fd %d is not a perf_event program\n", prog_fd);
47084724
return -1;
47094725
}
4710-
return ks_read_perf_count(perf_fd);
4726+
int64_t result = ks_read_perf_count(dup_fd);
4727+
close(dup_fd);
4728+
return result;
47114729
}
47124730
47134731
/* Print the current counter value for a named event to stdout.

tests/test_perf_event_attach.ml

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,19 @@ let test_perf_read_count_function_generated () =
263263
(contains_substr code "ks_read_perf_count: read failed on perf_fd");
264264
check bool "short read diagnostic present" true
265265
(contains_substr code "short read");
266-
check bool "ks_perf_read reads perf_fd under the lock" true
267-
(contains_substr code "Read perf_fd under the lock")
266+
check bool "ks_perf_read dups perf_fd under the lock" true
267+
(contains_substr code "Dup perf_fd under the lock")
268+
269+
let test_perf_read_detach_concurrent_window () =
270+
(* When detach runs concurrently with perf_read, perf_read must dup the fd
271+
* under the lock so that close(perf_fd) in detach cannot affect the read. *)
272+
let code = make_perf_code_with ~period:1000000L ~wakeup:1L in
273+
check bool "ks_perf_read dups perf_fd under the lock" true
274+
(contains_substr code "dup_fd = dup(cur->perf_fd)");
275+
check bool "ks_perf_read closes dup'd fd after reading" true
276+
(contains_substr code "close(dup_fd)");
277+
check bool "ks_perf_read skips detaching entries" true
278+
(contains_substr code "!cur->detaching && cur->perf_fd >= 0")
268279

269280
let test_perf_attach_event_function_generated () =
270281
(* attach(prog, perf_options{...}, 0) must generate ks_attach_perf_event which
@@ -293,6 +304,20 @@ let test_perf_attach_event_function_generated () =
293304
check bool "add_attachment performs atomic duplicate check" true
294305
(contains_substr code "Reject duplicate insertions atomically")
295306

307+
let test_detach_attach_concurrent_window () =
308+
(* During a detach, the entry stays in the list but is marked detaching=1.
309+
* A concurrent attach for the same prog_fd must succeed (not be blocked by
310+
* the still-present but detaching entry). *)
311+
let code = make_perf_code_with ~period:1000000L ~wakeup:1L in
312+
check bool "attachment_entry has detaching field" true
313+
(contains_substr code "int detaching;");
314+
check bool "add_attachment skips detaching entries in duplicate check" true
315+
(contains_substr code "!existing->detaching");
316+
check bool "detach marks entry as detaching before teardown" true
317+
(contains_substr code "entry->detaching = 1");
318+
check bool "detach re-locks to unlink and free entry after teardown" true
319+
(contains_substr code "Phase 2: teardown is complete")
320+
296321
(* ── Type-checking regression tests ───────────────────────────────────── *)
297322

298323
let parse_and_check source =
@@ -363,6 +388,8 @@ let tests = [
363388
test_case "perf_event_period_and_wakeup_custom" `Quick test_perf_event_period_and_wakeup_custom;
364389
test_case "perf_read_count_function_generated" `Quick test_perf_read_count_function_generated;
365390
test_case "perf_attach_event_function_generated" `Quick test_perf_attach_event_function_generated;
391+
test_case "perf_read_detach_concurrent_window" `Quick test_perf_read_detach_concurrent_window;
392+
test_case "detach_attach_concurrent_window" `Quick test_detach_attach_concurrent_window;
366393
test_case "standard_attach_uses_libbpf_error_checks" `Quick test_standard_attach_uses_libbpf_error_checks;
367394
]
368395

0 commit comments

Comments
 (0)