selftests/bpf: Convert lirc_mode2 to prog_tests and extend coverage

While porting, extend coverage of the BPF_LIRC_MODE2 attach/detach/
query API:

 - bpf_prog_attach() with invalid flags is rejected with -EINVAL
   and does not attach the program
 - bpf_prog_query() with invalid flags is rejected with -EINVAL
   without disturbing existing attachments
 - bpf_prog_query() reports the correct program id, not just count,
   at each step, via bpf_prog_get_info_by_fd()
 - a lirc chardev can hold more than one attached program: load a
   second, independent instance, attach it alongside the first,
   confirm both are reported by bpf_prog_query(), then detach it
   without disturbing the first program's attachment
 - detaching an already-detached program consistently fails with
   -ENOENT, for both the first and second program

Signed-off-by: Sean Young <sean@mess.org>
Assisted-by: Claude:claude-sonnet-5
Link: https://lore.kernel.org/bpf/20260820214332.89030-1-sean@mess.org
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
This commit is contained in:
Sean Young 2026-08-20 22:43:29 +01:00 committed by Kumar Kartikeya Dwivedi
parent fbc547151d
commit d761934c94
No known key found for this signature in database
GPG Key ID: 472D377B63542F83
7 changed files with 366 additions and 248 deletions

View File

@ -17,7 +17,6 @@ test_verifier_log
feature
urandom_read
test_sockmap
test_lirc_mode2_user
flow_dissector_load
test_tcpnotify_user
test_libbpf

View File

@ -125,7 +125,6 @@ TEST_FILES = xsk_prereqs.sh $(wildcard progs/btf_dump_test_case_*.c) \
# Order correspond to 'make run_tests' order
TEST_PROGS := test_kmod.sh \
test_lirc_mode2.sh \
test_bpftool_build.sh \
test_doc_build.sh \
test_xsk.sh \
@ -143,7 +142,6 @@ TEST_GEN_PROGS_EXTENDED = \
bench \
flow_dissector_load \
test_cpp \
test_lirc_mode2_user \
veristat \
xdp_features \
xdp_hw_metadata \
@ -340,7 +338,6 @@ $(OUTPUT)/test_sockmap: $(CGROUP_HELPERS) $(TESTING_HELPERS)
$(OUTPUT)/test_tcpnotify_user: $(CGROUP_HELPERS) $(TESTING_HELPERS) $(TRACE_HELPERS)
$(OUTPUT)/test_sock_fields: $(CGROUP_HELPERS) $(TESTING_HELPERS)
$(OUTPUT)/test_tag: $(TESTING_HELPERS)
$(OUTPUT)/test_lirc_mode2_user: $(TESTING_HELPERS)
$(OUTPUT)/flow_dissector_load: $(TESTING_HELPERS)
$(OUTPUT)/test_maps: $(TESTING_HELPERS)
$(OUTPUT)/test_verifier: $(TESTING_HELPERS) $(CAP_HELPERS) $(UNPRIV_HELPERS)

View File

@ -0,0 +1,334 @@
// SPDX-License-Identifier: GPL-2.0
// test ir decoder
//
// Copyright (C) 2018 Sean Young <sean@mess.org>
// A lirc chardev is a device representing a consumer IR (cir) device which
// can receive infrared signals from remote control and/or transmit IR.
//
// IR is sent as a series of pulses and space somewhat like morse code. The
// BPF program can decode this into scancodes so that rc-core can translate
// this into input key codes using the rc keymap.
//
// This test works by sending IR over rc-loopback, so the IR is processed by
// BPF and then decoded into scancodes. The lirc chardev must be the one
// associated with rc-loopback, see the output of ir-keytable(1).
//
// The following CONFIG options must be enabled for the test to succeed:
// CONFIG_RC_CORE=y
// CONFIG_BPF_LIRC_MODE2=y
// CONFIG_RC_LOOPBACK=y
// CONFIG_LIRC=y
#include <linux/input.h>
#include <linux/lirc.h>
#include <glob.h>
#include <limits.h>
#include <poll.h>
#include <test_progs.h>
#include "lirc_mode2.skel.h"
/*
* Read the DEVNAME= line out of the first uevent file that matches
* pattern, and turn it into a /dev/<name> path.
*/
static bool find_devname(const char *pattern, char *path, size_t path_sz)
{
glob_t gl = {};
bool found = false;
FILE *f;
if (glob(pattern, 0, NULL, &gl) || gl.gl_pathc == 0)
goto out;
f = fopen(gl.gl_pathv[0], "r");
if (!f)
goto out;
char line[256];
while (fgets(line, sizeof(line), f)) {
char *val;
if (strncmp(line, "DEVNAME=", 8))
continue;
val = line + 8;
val[strcspn(val, "\n")] = '\0';
snprintf(path, path_sz, "/dev/%s", val);
found = true;
break;
}
fclose(f);
out:
globfree(&gl);
return found;
}
/* Load rc-loopback and find the lirc and input chardevs it created. */
static bool find_loopback_devices(char *lirc_path, char *input_path,
size_t path_sz)
{
glob_t gl = {};
bool found = false;
/* Ignore failure, we check for the resulting devices below. */
SYS_NOFAIL("modprobe rc-loopback > /dev/null 2>&1");
if (glob("/sys/class/rc/rc*", 0, NULL, &gl))
goto out;
for (size_t i = 0; i < gl.gl_pathc; i++) {
const char *rcdir = gl.gl_pathv[i];
char uevent_path[PATH_MAX];
char uevent[4096];
char pattern[PATH_MAX];
FILE *f;
size_t n;
snprintf(uevent_path, sizeof(uevent_path), "%s/uevent", rcdir);
f = fopen(uevent_path, "r");
if (!f)
continue;
n = fread(uevent, 1, sizeof(uevent) - 1, f);
fclose(f);
uevent[n] = '\0';
if (!strstr(uevent, "DRV_NAME=rc-loopback"))
continue;
snprintf(pattern, sizeof(pattern), "%s/lirc*/uevent", rcdir);
if (!find_devname(pattern, lirc_path, path_sz))
continue;
snprintf(pattern, sizeof(pattern), "%s/input*/event*/uevent", rcdir);
if (!find_devname(pattern, input_path, path_sz))
continue;
found = true;
break;
}
out:
if (!found) {
fprintf(stderr, "No rc devices found\n");
fprintf(stderr, "Enable CONFIG_RC_LOOPBACK and CONFIG_BPF_LIRC_MODE2\n");
}
globfree(&gl);
return found;
}
void test_lirc_mode2(void)
{
char lirc_path[PATH_MAX], input_path[PATH_MAX];
int lircfd = -1, inputfd = -1, progfd, progfd2 = -1;
struct lirc_mode2 *skel = NULL, *skel2 = NULL;
__u32 prog_ids[10], prog_flags[10], prog_cnt;
struct bpf_prog_info info;
__u32 info_len, prog_id, prog_id2;
int testir1 = 0x8ead; /* keydown flag (0x8000) | scancode 0xead */
int testir2 = 0x4081; /* pointer_rel flag (0x4000) | rel_x=1 | rel_y=1 */
struct input_event event;
struct pollfd pfd = {};
int ret;
if (getuid() != 0) {
test__skip();
return;
}
if (!find_loopback_devices(lirc_path, input_path, sizeof(lirc_path))) {
test__skip();
return;
}
skel = lirc_mode2__open_and_load();
if (!ASSERT_OK_PTR(skel, "lirc_mode2__open_and_load"))
return;
progfd = bpf_program__fd(skel->progs.bpf_decoder);
memset(&info, 0, sizeof(info));
info_len = sizeof(info);
ret = bpf_prog_get_info_by_fd(progfd, &info, &info_len);
if (!ASSERT_OK(ret, "get first program's info"))
goto out;
prog_id = info.id;
lircfd = open(lirc_path, O_RDWR | O_NONBLOCK);
if (!ASSERT_GE(lircfd, 0, "open lirc device"))
goto out;
/* Try to detach it before it was ever attached, should fail. */
ret = bpf_prog_detach2(progfd, lircfd, BPF_LIRC_MODE2);
if (!ASSERT_EQ(ret, -ENOENT, "detach unattached program"))
goto out;
inputfd = open(input_path, O_RDONLY | O_NONBLOCK);
if (!ASSERT_GE(inputfd, 0, "open input device"))
goto out;
prog_cnt = ARRAY_SIZE(prog_ids);
ret = bpf_prog_query(lircfd, BPF_LIRC_MODE2, 0, prog_flags, prog_ids,
&prog_cnt);
if (!ASSERT_OK(ret, "query programs before attach"))
goto out;
if (!ASSERT_EQ(prog_cnt, 0, "no programs should be attached yet"))
goto out;
/* Invalid attach flags must be rejected, and must not attach. */
ret = bpf_prog_attach(progfd, lircfd, BPF_LIRC_MODE2, 1);
if (!ASSERT_EQ(ret, -EINVAL, "attach with invalid flags"))
goto out;
prog_cnt = ARRAY_SIZE(prog_ids);
ret = bpf_prog_query(lircfd, BPF_LIRC_MODE2, 0, prog_flags, prog_ids,
&prog_cnt);
if (!ASSERT_OK(ret, "query programs after rejected attach"))
goto out;
if (!ASSERT_EQ(prog_cnt, 0, "rejected attach should not attach"))
goto out;
ret = bpf_prog_attach(progfd, lircfd, BPF_LIRC_MODE2, 0);
if (!ASSERT_OK(ret, "attach program to lirc device"))
goto out;
/* Invalid query flags must be rejected too, without upsetting state. */
prog_cnt = ARRAY_SIZE(prog_ids);
ret = bpf_prog_query(lircfd, BPF_LIRC_MODE2, 1, prog_flags, prog_ids,
&prog_cnt);
ASSERT_EQ(ret, -EINVAL, "query with invalid flags");
prog_cnt = ARRAY_SIZE(prog_ids);
ret = bpf_prog_query(lircfd, BPF_LIRC_MODE2, 0, prog_flags, prog_ids,
&prog_cnt);
if (!ASSERT_OK(ret, "query programs after attach"))
goto out_detach;
if (!ASSERT_EQ(prog_cnt, 1, "one program should be attached"))
goto out_detach;
ASSERT_EQ(prog_ids[0], prog_id, "queried id should match attached program");
/* Write raw IR */
ret = write(lircfd, &testir1, sizeof(testir1));
if (!ASSERT_EQ(ret, sizeof(testir1), "send test IR message 1"))
goto out_detach;
pfd.fd = inputfd;
pfd.events = POLLIN;
for (;;) {
poll(&pfd, 1, 100);
/* Read decoded IR */
ret = read(inputfd, &event, sizeof(event));
if (!ASSERT_EQ(ret, sizeof(event), "read decoded IR 1"))
goto out_detach;
if (event.type == EV_MSC && event.code == MSC_SCAN &&
event.value == 0xead)
break;
}
/* Write raw IR */
ret = write(lircfd, &testir2, sizeof(testir2));
if (!ASSERT_EQ(ret, sizeof(testir2), "send test IR message 2"))
goto out_detach;
for (;;) {
poll(&pfd, 1, 100);
/* Read decoded IR */
ret = read(inputfd, &event, sizeof(event));
if (!ASSERT_EQ(ret, sizeof(event), "read decoded IR 2"))
goto out_detach;
if (event.type == EV_REL && event.code == REL_Y &&
event.value == 1)
break;
}
prog_cnt = ARRAY_SIZE(prog_ids);
ret = bpf_prog_query(lircfd, BPF_LIRC_MODE2, 0, prog_flags, prog_ids,
&prog_cnt);
if (!ASSERT_OK(ret, "query programs after IR was decoded"))
goto out_detach;
if (!ASSERT_EQ(prog_cnt, 1, "one program should still be attached"))
goto out_detach;
/*
* The lirc chardev can hold more than one attached program at once.
* Load a second, independent instance and check it can be attached
* alongside the first, queried, and then detached on its own
* without disturbing the first program's attachment.
*/
skel2 = lirc_mode2__open_and_load();
if (!ASSERT_OK_PTR(skel2, "lirc_mode2__open_and_load (2nd)"))
goto out_detach;
progfd2 = bpf_program__fd(skel2->progs.bpf_decoder);
memset(&info, 0, sizeof(info));
info_len = sizeof(info);
ret = bpf_prog_get_info_by_fd(progfd2, &info, &info_len);
if (!ASSERT_OK(ret, "get second program's info"))
goto out_detach;
prog_id2 = info.id;
ret = bpf_prog_attach(progfd2, lircfd, BPF_LIRC_MODE2, 0);
if (!ASSERT_OK(ret, "attach second program to lirc device"))
goto out_detach;
prog_cnt = ARRAY_SIZE(prog_ids);
ret = bpf_prog_query(lircfd, BPF_LIRC_MODE2, 0, prog_flags, prog_ids,
&prog_cnt);
if (!ASSERT_OK(ret, "query programs after second attach"))
goto out_detach2;
if (!ASSERT_EQ(prog_cnt, 2, "two programs should be attached"))
goto out_detach2;
ASSERT_TRUE((prog_ids[0] == prog_id && prog_ids[1] == prog_id2) ||
(prog_ids[0] == prog_id2 && prog_ids[1] == prog_id),
"queried ids should be the two attached programs");
/* Detach the second program; the first should remain attached. */
ret = bpf_prog_detach2(progfd2, lircfd, BPF_LIRC_MODE2);
if (!ASSERT_OK(ret, "detach second program"))
goto out_detach2;
/* Detaching an already-detached program should now fail. */
ret = bpf_prog_detach2(progfd2, lircfd, BPF_LIRC_MODE2);
ASSERT_EQ(ret, -ENOENT, "detach second program again");
prog_cnt = ARRAY_SIZE(prog_ids);
ret = bpf_prog_query(lircfd, BPF_LIRC_MODE2, 0, prog_flags, prog_ids,
&prog_cnt);
if (!ASSERT_OK(ret, "query programs after second detach"))
goto out_detach;
if (!ASSERT_EQ(prog_cnt, 1, "one program should remain attached"))
goto out_detach;
ASSERT_EQ(prog_ids[0], prog_id, "remaining program should be the first one");
out_detach:
/* Let's try detaching it now it is actually attached. */
ret = bpf_prog_detach2(progfd, lircfd, BPF_LIRC_MODE2);
ASSERT_OK(ret, "detach program from lirc device");
/* Detaching it again should now fail the same way. */
ret = bpf_prog_detach2(progfd, lircfd, BPF_LIRC_MODE2);
ASSERT_EQ(ret, -ENOENT, "detach program from lirc device again");
goto out;
out_detach2:
/* Best-effort cleanup of the second program before bailing out. */
bpf_prog_detach2(progfd2, lircfd, BPF_LIRC_MODE2);
goto out_detach;
out:
if (inputfd >= 0)
close(inputfd);
if (lircfd >= 0)
close(lircfd);
lirc_mode2__destroy(skel2);
lirc_mode2__destroy(skel);
}

View File

@ -0,0 +1,32 @@
// SPDX-License-Identifier: GPL-2.0
// test ir decoder
//
// Copyright (C) 2018 Sean Young <sean@mess.org>
#include <linux/bpf.h>
#include <linux/lirc.h>
#include <bpf/bpf_helpers.h>
SEC("lirc_mode2")
int bpf_decoder(unsigned int *sample)
{
if (LIRC_IS_PULSE(*sample)) {
unsigned int duration = LIRC_VALUE(*sample);
/*
* Flag bits picked deliberately low: rc-loopback simulates
* a receiver overflow for any pulse over MS_TO_US(50) (see
* loop_tx_ir() in rc-loopback.c), which would silently
* swallow the sample before it ever reaches this decoder.
*/
if (duration & 0x8000)
bpf_rc_keydown(sample, 0x40, duration & 0x3fff, 0);
if (duration & 0x4000)
bpf_rc_pointer_rel(sample, (duration >> 7) & 0x7f,
duration & 0x7f);
}
return 0;
}
char _license[] SEC("license") = "GPL";

View File

@ -1,26 +0,0 @@
// SPDX-License-Identifier: GPL-2.0
// test ir decoder
//
// Copyright (C) 2018 Sean Young <sean@mess.org>
#include <linux/bpf.h>
#include <linux/lirc.h>
#include <bpf/bpf_helpers.h>
SEC("lirc_mode2")
int bpf_decoder(unsigned int *sample)
{
if (LIRC_IS_PULSE(*sample)) {
unsigned int duration = LIRC_VALUE(*sample);
if (duration & 0x1000)
bpf_rc_keydown(sample, 0x40, duration & 0xffff, 0);
if (duration & 0x2000)
bpf_rc_pointer_rel(sample, (duration >> 8) & 0xff,
duration & 0xff);
}
return 0;
}
char _license[] SEC("license") = "GPL";

View File

@ -1,41 +0,0 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# Kselftest framework requirement - SKIP code is 4.
ksft_skip=4
ret=$ksft_skip
msg="skip all tests:"
if [ $UID != 0 ]; then
echo $msg please run this as root >&2
exit $ksft_skip
fi
GREEN='\033[0;92m'
RED='\033[0;31m'
NC='\033[0m' # No Color
modprobe rc-loopback
for i in /sys/class/rc/rc*
do
if grep -q DRV_NAME=rc-loopback $i/uevent
then
LIRCDEV=$(grep DEVNAME= $i/lirc*/uevent | sed sQDEVNAME=Q/dev/Q)
INPUTDEV=$(grep DEVNAME= $i/input*/event*/uevent | sed sQDEVNAME=Q/dev/Q)
fi
done
if [ -n "$LIRCDEV" ];
then
TYPE=lirc_mode2
./test_lirc_mode2_user $LIRCDEV $INPUTDEV
ret=$?
if [ $ret -ne 0 ]; then
echo -e ${RED}"FAIL: $TYPE"${NC}
else
echo -e ${GREEN}"PASS: $TYPE"${NC}
fi
fi
exit $ret

View File

@ -1,177 +0,0 @@
// SPDX-License-Identifier: GPL-2.0
// test ir decoder
//
// Copyright (C) 2018 Sean Young <sean@mess.org>
// A lirc chardev is a device representing a consumer IR (cir) device which
// can receive infrared signals from remote control and/or transmit IR.
//
// IR is sent as a series of pulses and space somewhat like morse code. The
// BPF program can decode this into scancodes so that rc-core can translate
// this into input key codes using the rc keymap.
//
// This test works by sending IR over rc-loopback, so the IR is processed by
// BPF and then decoded into scancodes. The lirc chardev must be the one
// associated with rc-loopback, see the output of ir-keytable(1).
//
// The following CONFIG options must be enabled for the test to succeed:
// CONFIG_RC_CORE=y
// CONFIG_BPF_RAWIR_EVENT=y
// CONFIG_RC_LOOPBACK=y
// Steps:
// 1. Open the /dev/lircN device for rc-loopback (given on command line)
// 2. Attach bpf_lirc_mode2 program which decodes some IR.
// 3. Send some IR to the same IR device; since it is loopback, this will
// end up in the bpf program
// 4. bpf program should decode IR and report keycode
// 5. We can read keycode from same /dev/lirc device
#include <linux/bpf.h>
#include <linux/input.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "bpf_util.h"
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include "testing_helpers.h"
int main(int argc, char **argv)
{
struct bpf_object *obj;
int ret, lircfd, progfd, inputfd;
int testir1 = 0x1ead;
int testir2 = 0x2101;
u32 prog_ids[10], prog_flags[10], prog_cnt;
if (argc != 3) {
printf("Usage: %s /dev/lircN /dev/input/eventM\n", argv[0]);
return 2;
}
ret = bpf_prog_test_load("test_lirc_mode2_kern.bpf.o",
BPF_PROG_TYPE_LIRC_MODE2, &obj, &progfd);
if (ret) {
printf("Failed to load bpf program\n");
return 1;
}
lircfd = open(argv[1], O_RDWR | O_NONBLOCK);
if (lircfd == -1) {
printf("failed to open lirc device %s: %m\n", argv[1]);
return 1;
}
/* Let's try detach it before it was ever attached */
ret = bpf_prog_detach2(progfd, lircfd, BPF_LIRC_MODE2);
if (ret != -ENOENT) {
printf("bpf_prog_detach2 not attached should fail: %m\n");
return 1;
}
inputfd = open(argv[2], O_RDONLY | O_NONBLOCK);
if (inputfd == -1) {
printf("failed to open input device %s: %m\n", argv[1]);
return 1;
}
prog_cnt = 10;
ret = bpf_prog_query(lircfd, BPF_LIRC_MODE2, 0, prog_flags, prog_ids,
&prog_cnt);
if (ret) {
printf("Failed to query bpf programs on lirc device: %m\n");
return 1;
}
if (prog_cnt != 0) {
printf("Expected nothing to be attached\n");
return 1;
}
ret = bpf_prog_attach(progfd, lircfd, BPF_LIRC_MODE2, 0);
if (ret) {
printf("Failed to attach bpf to lirc device: %m\n");
return 1;
}
/* Write raw IR */
ret = write(lircfd, &testir1, sizeof(testir1));
if (ret != sizeof(testir1)) {
printf("Failed to send test IR message: %m\n");
return 1;
}
struct pollfd pfd = { .fd = inputfd, .events = POLLIN };
struct input_event event;
for (;;) {
poll(&pfd, 1, 100);
/* Read decoded IR */
ret = read(inputfd, &event, sizeof(event));
if (ret != sizeof(event)) {
printf("Failed to read decoded IR: %m\n");
return 1;
}
if (event.type == EV_MSC && event.code == MSC_SCAN &&
event.value == 0x1ead) {
break;
}
}
/* Write raw IR */
ret = write(lircfd, &testir2, sizeof(testir2));
if (ret != sizeof(testir2)) {
printf("Failed to send test IR message: %m\n");
return 1;
}
for (;;) {
poll(&pfd, 1, 100);
/* Read decoded IR */
ret = read(inputfd, &event, sizeof(event));
if (ret != sizeof(event)) {
printf("Failed to read decoded IR: %m\n");
return 1;
}
if (event.type == EV_REL && event.code == REL_Y &&
event.value == 1 ) {
break;
}
}
prog_cnt = 10;
ret = bpf_prog_query(lircfd, BPF_LIRC_MODE2, 0, prog_flags, prog_ids,
&prog_cnt);
if (ret) {
printf("Failed to query bpf programs on lirc device: %m\n");
return 1;
}
if (prog_cnt != 1) {
printf("Expected one program to be attached\n");
return 1;
}
/* Let's try detaching it now it is actually attached */
ret = bpf_prog_detach2(progfd, lircfd, BPF_LIRC_MODE2);
if (ret) {
printf("bpf_prog_detach2: returned %m\n");
return 1;
}
return 0;
}