// SPDX-License-Identifier: GPL-2.0 // test ir decoder // // Copyright (C) 2018 Sean Young // 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 #include #include #include #include #include #include "lirc_mode2.skel.h" /* * Read the DEVNAME= line out of the first uevent file that matches * pattern, and turn it into a /dev/ 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); }