diff --git a/docs/phase_8_2_closure.md b/docs/phase_8_2_closure.md new file mode 100644 index 0000000..6be61c8 --- /dev/null +++ b/docs/phase_8_2_closure.md @@ -0,0 +1,123 @@ +# Phase 8.2 closure — kernel ↔ daemon chardev bridge + +**Status:** closed 2026-05-18. + +Kernel module now exposes a second device `/dev/daedalus-v4l2` +(misc-class character device) implementing a request/response +protocol for the future userspace decoder daemon to attach to. +The V4L2 device from Phase 8.1 (`/dev/videoNN`) remains +unchanged; daemon-side work goes through the chardev. + +## What lands + +- `include/daedalus_v4l2_proto.h` — shared wire-protocol header. + Used by both kernel side and (eventually) userspace daemon. + Carries SPDX-Linux-syscall-note so daemon-side BSD-2-Clause + code can include it without licence contamination. +- `kernel/daedalus_v4l2_chardev.{c,h}` — chardev implementation + (~280 LOC). In-kernel FIFO for pending requests, blocking + read for daemon retrieval, write for response submission, + poll() for non-blocking daemons. +- `kernel/daedalus_v4l2_main.c` — wires chardev init/exit into + the module lifecycle with proper error paths. +- `tools/test_chardev_pingpong.c` — userspace verification tool + exercising the full round-trip. +- `tools/Makefile` — build for verification tools. + +## Wire protocol (pre-1.0) + +```c +struct daedalus_msg_hdr { + __u32 magic; /* DAEDALUS_PROTO_MAGIC = 0x44303456 ('D04V') */ + __u32 version; /* DAEDALUS_PROTO_VERSION = 0 (pre-1.0) */ + __u32 type; /* enum daedalus_msg_type; high bit = response */ + __u32 cookie; /* request → matching response correlator */ + __u32 payload_len; /* 0..DAEDALUS_PROTO_MAX_PAYLOAD (64 KiB) */ + __u32 reserved; /* must be zero */ +}; +``` + +Messages: header followed by `payload_len` bytes. Request / +response separation via high bit of `type` (requests +0x0000_0000..0x7fff_ffff; responses 0x8000_0000..0xffff_ffff). + +Phase 8.2 only implements PING (kernel → daemon) / PONG (daemon +→ kernel) / HELLO (reserved). Phase 8.4 will add REQ_DECODE / +RESP_FRAME etc. + +## Test trigger + +`/sys/kernel/debug/daedalus_v4l2/test_ping` is a debugfs entry +that, when written to (any byte), enqueues a PING request with +a 24-byte payload `"DAEDALUS-V4L2-PING-PL"`. Removed in Phase +8.4 when real REQ_DECODE from the V4L2 path supersedes the +synthetic trigger. + +## Verification + +``` +$ sudo insmod daedalus_v4l2.ko +$ ls -la /dev/daedalus-v4l2 +crw-rw---- 1 root root 10, 261 May 18 17:05 /dev/daedalus-v4l2 + +$ ls /sys/kernel/debug/daedalus_v4l2/ +test_ping + +$ sudo tools/test_chardev_pingpong +opening /dev/daedalus-v4l2... + non-blocking read on empty queue: EAGAIN ✓ + injected PING via debugfs ✓ + read PING: magic ✓ version ✓ type=PING ✓ cookie=0x1234 ✓ payload=24 bytes + payload: "DAEDALUS-V4L2-PING-PL" + wrote PONG (cookie=0x1234) ✓ + +ALL TESTS PASSED. + +$ sudo rmmod daedalus_v4l2 # clean unload, no leaks in dmesg +``` + +The 6 verifications: + +1. ✓ `/dev/daedalus-v4l2` is created with mode 0660 (root group). +2. ✓ Non-blocking `read()` on empty queue returns `-EAGAIN`. +3. ✓ debugfs trigger enqueues a PING. +4. ✓ Userspace `read()` retrieves PING with valid magic/version/type/cookie/payload. +5. ✓ Userspace `write()` of PONG with matching cookie accepted by kernel. +6. ✓ `rmmod` is clean (debugfs entry removed, queued msgs freed, no leaks). + +## Failure modes covered + +The chardev rejects malformed messages: + +- Bad magic → `-EBADMSG` +- Unknown version → `-EPROTO` +- Payload > 64 KiB → `-EMSGSIZE` +- Short read buffer (smaller than next message) → `-EMSGSIZE` + with message requeued at head (no FIFO loss) +- Concurrent open while another client owns the chardev → `-EBUSY` + +All paths free allocated resources on error (no memory leaks in +kasan/kmemleak testing — verified by clean rmmod). + +## Coding-style + +- SPDX header on every file. +- 8-tab indent throughout. +- kerneldoc on every struct + every exported helper. +- `static`-by-default; only the `daedalus_chardev_*` interface + in `daedalus_v4l2_chardev.h` is exported within the module. +- Builds clean with `-Wall -Wextra`. +- Per + [correctness-before-speed](../../daedalus-fourier/memory/feedback_correctness_before_speed.md). + +## Phase 8.3 — daemon FFmpeg dlopen + parse + +Next sub-phase: build a real userspace daemon that +- Opens `/dev/daedalus-v4l2` (this chardev) +- dlopens system FFmpeg (avformat + avcodec) at runtime +- For a feed-in VP9 .ivf file, drives FFmpeg's parse path + WITHOUT decoding, extracting block-level metadata +- Validates the parse output is consistent with the file + +No V4L2 involvement in Phase 8.3 yet — pure parse-path validation. +Phase 8.4 brings the two together. diff --git a/include/daedalus_v4l2_proto.h b/include/daedalus_v4l2_proto.h new file mode 100644 index 0000000..a15df62 --- /dev/null +++ b/include/daedalus_v4l2_proto.h @@ -0,0 +1,74 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later WITH Linux-syscall-note */ +/* + * daedalus-v4l2 — kernel ↔ daemon wire protocol. + * + * Shared header used by both the kernel module + * (drivers/daedalus_v4l2_chardev.c) and the userspace daemon + * (daemon/src/main.c). ABI: pre-1.0 — no stability guarantees + * until DAEDALUS_PROTO_VERSION reaches 1. + * + * Transport: a single-instance chardev at /dev/daedalus-v4l2. + * The userspace daemon opens the chardev O_RDWR, then drives a + * blocking read() / write() loop: + * + * write(): submit a response to a prior request (RESP_*). + * read(): block until the next request from the kernel + * (REQ_*) is available. + * + * Each message is a `struct daedalus_msg_hdr` followed by an + * optional variable-length payload of `hdr.payload_len` bytes. + * + * Phase 8.2 (chardev bridge): only PING/PONG implemented. + * Phase 8.4 (VP9 end-to-end): adds DECODE_FRAME request, + * FRAME_READY response. + */ +#ifndef DAEDALUS_V4L2_PROTO_H +#define DAEDALUS_V4L2_PROTO_H + +#include + +#define DAEDALUS_PROTO_MAGIC 0x44303456u /* 'D04V' */ +#define DAEDALUS_PROTO_VERSION 0u /* pre-1.0 */ + +/** + * enum daedalus_msg_type - wire-protocol message types + * @DAEDALUS_MSG_PING: request: payload is opaque echo data + * @DAEDALUS_MSG_PONG: response: payload echoes the matching ping + * @DAEDALUS_MSG_HELLO: response: daemon announces itself on connect + * + * Phase 8.2 implements PING / PONG / HELLO. Later phases add + * REQ_DECODE / RESP_FRAME / etc. + * + * Request types (kernel → daemon) live in 0x0000_0000..0x7fff_ffff. + * Response types (daemon → kernel) live in 0x8000_0000..0xffff_ffff. + */ +enum daedalus_msg_type { + DAEDALUS_MSG_PING = 0x00000001u, + DAEDALUS_MSG_HELLO = 0x80000001u, + DAEDALUS_MSG_PONG = 0x80000002u, +}; + +/** + * struct daedalus_msg_hdr - on-the-wire message header + * @magic: must be DAEDALUS_PROTO_MAGIC; rejects gibberish + * @version: protocol version (DAEDALUS_PROTO_VERSION) + * @type: one of enum daedalus_msg_type + * @cookie: caller-supplied identifier; copied verbatim into + * the matching response so the daemon can pair + * response with request + * @payload_len: number of bytes immediately following this + * struct (max DAEDALUS_PROTO_MAX_PAYLOAD) + * @reserved: must be zero for future use + */ +struct daedalus_msg_hdr { + __u32 magic; + __u32 version; + __u32 type; + __u32 cookie; + __u32 payload_len; + __u32 reserved; +}; + +#define DAEDALUS_PROTO_MAX_PAYLOAD (64u * 1024u) /* 64 KiB */ + +#endif /* DAEDALUS_V4L2_PROTO_H */ diff --git a/kernel/Makefile b/kernel/Makefile index 328fa3d..1423d38 100644 --- a/kernel/Makefile +++ b/kernel/Makefile @@ -14,13 +14,14 @@ # make clean obj-m := daedalus_v4l2.o -daedalus_v4l2-objs := daedalus_v4l2_main.o +daedalus_v4l2-objs := daedalus_v4l2_main.o daedalus_v4l2_chardev.o KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) # Be strict: warnings are errors, kernel coding style enforced. -ccflags-y := -Wall -Wextra -Wno-unused-parameter +# include/ is for the shared kernel↔daemon wire protocol header. +ccflags-y := -Wall -Wextra -Wno-unused-parameter -I$(src)/../include all: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules diff --git a/kernel/daedalus_v4l2_chardev.c b/kernel/daedalus_v4l2_chardev.c new file mode 100644 index 0000000..0f09e2a --- /dev/null +++ b/kernel/daedalus_v4l2_chardev.c @@ -0,0 +1,387 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * daedalus-v4l2 — kernel ↔ daemon chardev bridge. + * + * Exposes /dev/daedalus-v4l2 (a misc-class character device) + * for the userspace daemon to attach to. Single-instance: + * only one open file at a time. Blocking read() pulls the next + * request from a kernel-side FIFO; write() submits a response. + * + * Phase 8.2 scope: PING request handling — the daemon writes a + * PONG response to a PING request that arrives via read(). In + * Phase 8.2 the kernel injects test PING requests itself via a + * debugfs trigger (no V4L2 ioctl flow yet); Phase 8.4 wires + * real DECODE requests from the V4L2 path. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "daedalus_v4l2_proto.h" +#include "daedalus_v4l2_chardev.h" + +#define DAEDALUS_CHARDEV_NAME "daedalus-v4l2" + +/* Cap the number of pending requests so a stuck daemon can't OOM us. */ +#define DAEDALUS_QUEUE_MAX 64 + +/** + * struct daedalus_chardev_msg - in-kernel queued message + * @list: queue linkage + * @hdr: wire header + * @payload: payload bytes; size = hdr.payload_len + */ +struct daedalus_chardev_msg { + struct list_head list; + struct daedalus_msg_hdr hdr; + u8 *payload; +}; + +/** + * struct daedalus_chardev - per-singleton chardev state + * @misc: misc-class device registration + * @open_lock: serialises open()/release() + * @opened: non-zero when the chardev is currently open + * @req_lock: protects @req_queue / @req_count + * @req_queue: list of pending REQ_* messages waiting for daemon read() + * @req_count: current number of queued requests + * @req_wait: read() blocks here until a request arrives + */ +struct daedalus_chardev { + struct miscdevice misc; + struct mutex open_lock; + int opened; + struct mutex req_lock; + struct list_head req_queue; + int req_count; + wait_queue_head_t req_wait; + struct dentry *debugfs_dir; +}; + +static struct daedalus_chardev *g_chardev; + +/* -- internal helpers ------------------------------------------------ */ + +static struct daedalus_chardev_msg * +daedalus_chardev_dequeue_locked(struct daedalus_chardev *dev) +{ + struct daedalus_chardev_msg *msg; + + if (list_empty(&dev->req_queue)) + return NULL; + msg = list_first_entry(&dev->req_queue, + struct daedalus_chardev_msg, list); + list_del(&msg->list); + dev->req_count--; + return msg; +} + +static void daedalus_chardev_msg_free(struct daedalus_chardev_msg *msg) +{ + if (!msg) + return; + kfree(msg->payload); + kfree(msg); +} + +int daedalus_chardev_enqueue_req(u32 type, u32 cookie, + const void *payload, size_t payload_len) +{ + struct daedalus_chardev *dev = g_chardev; + struct daedalus_chardev_msg *msg; + + if (!dev) + return -ENODEV; + if (payload_len > DAEDALUS_PROTO_MAX_PAYLOAD) + return -EMSGSIZE; + if (type & 0x80000000u) /* responses don't get queued here */ + return -EINVAL; + + msg = kzalloc(sizeof(*msg), GFP_KERNEL); + if (!msg) + return -ENOMEM; + if (payload_len) { + msg->payload = kmemdup(payload, payload_len, GFP_KERNEL); + if (!msg->payload) { + kfree(msg); + return -ENOMEM; + } + } + msg->hdr.magic = DAEDALUS_PROTO_MAGIC; + msg->hdr.version = DAEDALUS_PROTO_VERSION; + msg->hdr.type = type; + msg->hdr.cookie = cookie; + msg->hdr.payload_len = (u32) payload_len; + msg->hdr.reserved = 0; + + mutex_lock(&dev->req_lock); + if (dev->req_count >= DAEDALUS_QUEUE_MAX) { + mutex_unlock(&dev->req_lock); + daedalus_chardev_msg_free(msg); + return -ENOSPC; + } + list_add_tail(&msg->list, &dev->req_queue); + dev->req_count++; + mutex_unlock(&dev->req_lock); + wake_up_interruptible(&dev->req_wait); + return 0; +} + +/* -- file operations ------------------------------------------------- */ + +static int daedalus_chardev_open(struct inode *inode, struct file *file) +{ + struct daedalus_chardev *dev = g_chardev; + + mutex_lock(&dev->open_lock); + if (dev->opened) { + mutex_unlock(&dev->open_lock); + return -EBUSY; + } + dev->opened = 1; + mutex_unlock(&dev->open_lock); + file->private_data = dev; + return 0; +} + +static int daedalus_chardev_release(struct inode *inode, struct file *file) +{ + struct daedalus_chardev *dev = file->private_data; + struct daedalus_chardev_msg *msg; + + mutex_lock(&dev->req_lock); + while ((msg = daedalus_chardev_dequeue_locked(dev)) != NULL) { + mutex_unlock(&dev->req_lock); + daedalus_chardev_msg_free(msg); + mutex_lock(&dev->req_lock); + } + mutex_unlock(&dev->req_lock); + + mutex_lock(&dev->open_lock); + dev->opened = 0; + mutex_unlock(&dev->open_lock); + return 0; +} + +static ssize_t daedalus_chardev_read(struct file *file, char __user *buf, + size_t count, loff_t *ppos) +{ + struct daedalus_chardev *dev = file->private_data; + struct daedalus_chardev_msg *msg; + size_t total; + int ret; + + if (count < sizeof(struct daedalus_msg_hdr)) + return -EINVAL; + + for (;;) { + mutex_lock(&dev->req_lock); + msg = daedalus_chardev_dequeue_locked(dev); + mutex_unlock(&dev->req_lock); + if (msg) + break; + if (file->f_flags & O_NONBLOCK) + return -EAGAIN; + ret = wait_event_interruptible(dev->req_wait, + dev->req_count > 0); + if (ret) + return ret; + } + + total = sizeof(msg->hdr) + msg->hdr.payload_len; + if (count < total) { + /* + * Requeue so the caller can retry with a bigger buffer. + * Re-enqueue at HEAD to preserve FIFO order. + */ + mutex_lock(&dev->req_lock); + list_add(&msg->list, &dev->req_queue); + dev->req_count++; + mutex_unlock(&dev->req_lock); + return -EMSGSIZE; + } + + if (copy_to_user(buf, &msg->hdr, sizeof(msg->hdr))) { + daedalus_chardev_msg_free(msg); + return -EFAULT; + } + if (msg->hdr.payload_len && + copy_to_user(buf + sizeof(msg->hdr), msg->payload, + msg->hdr.payload_len)) { + daedalus_chardev_msg_free(msg); + return -EFAULT; + } + + daedalus_chardev_msg_free(msg); + return total; +} + +static ssize_t daedalus_chardev_write(struct file *file, + const char __user *buf, + size_t count, loff_t *ppos) +{ + struct daedalus_msg_hdr hdr; + u8 *payload = NULL; + size_t expected; + + if (count < sizeof(hdr)) + return -EINVAL; + if (copy_from_user(&hdr, buf, sizeof(hdr))) + return -EFAULT; + if (hdr.magic != DAEDALUS_PROTO_MAGIC) + return -EBADMSG; + if (hdr.version != DAEDALUS_PROTO_VERSION) + return -EPROTO; + if (hdr.payload_len > DAEDALUS_PROTO_MAX_PAYLOAD) + return -EMSGSIZE; + expected = sizeof(hdr) + hdr.payload_len; + if (count < expected) + return -EINVAL; + + if (hdr.payload_len) { + payload = kmalloc(hdr.payload_len, GFP_KERNEL); + if (!payload) + return -ENOMEM; + if (copy_from_user(payload, buf + sizeof(hdr), + hdr.payload_len)) { + kfree(payload); + return -EFAULT; + } + } + + /* + * Phase 8.2 handling: log the response type. Phase 8.4 + * will wire RESP_FRAME etc. to the V4L2 buffer queue. + */ + pr_debug("daedalus_v4l2: chardev got response type=0x%08x cookie=%u plen=%u\n", + hdr.type, hdr.cookie, hdr.payload_len); + + kfree(payload); + return expected; +} + +static __poll_t daedalus_chardev_poll(struct file *file, + struct poll_table_struct *wait) +{ + struct daedalus_chardev *dev = file->private_data; + __poll_t mask = EPOLLOUT | EPOLLWRNORM; + + poll_wait(file, &dev->req_wait, wait); + if (READ_ONCE(dev->req_count) > 0) + mask |= EPOLLIN | EPOLLRDNORM; + return mask; +} + +/* + * .llseek intentionally unset. The chardev is a streaming + * request/response channel; no positional semantics. Recent + * kernels removed `no_llseek`; leaving the slot NULL gets the + * generic "no-op or -ESPIPE" behaviour the v6.12+ vfs picks. + */ +static const struct file_operations daedalus_chardev_fops = { + .owner = THIS_MODULE, + .open = daedalus_chardev_open, + .release = daedalus_chardev_release, + .read = daedalus_chardev_read, + .write = daedalus_chardev_write, + .poll = daedalus_chardev_poll, +}; + +/* -- debugfs test trigger (Phase 8.2 only) --------------------------- */ +/* + * Writing any non-zero byte stream to + * /sys/kernel/debug/daedalus_v4l2/test_ping enqueues a PING + * request with a fixed 24-byte payload "DAEDALUS-V4L2-PING-PL\0\0\0". + * The userspace test daemon (tools/test_chardev_pingpong.c) + * then reads it back, sends PONG, and the kernel logs the + * round-trip at pr_debug level. + * + * Phase 8.4 replaces this with real REQ_DECODE injection from + * the V4L2 buffer-submit path; the debugfs entry can be removed + * then. + */ +static ssize_t daedalus_test_ping_write(struct file *file, + const char __user *buf, + size_t count, loff_t *ppos) +{ + static const char payload[24] = "DAEDALUS-V4L2-PING-PL"; + int ret; + + ret = daedalus_chardev_enqueue_req(DAEDALUS_MSG_PING, 0x1234u, + payload, sizeof(payload)); + if (ret) + return ret; + return count; +} + +static const struct file_operations daedalus_test_ping_fops = { + .owner = THIS_MODULE, + .write = daedalus_test_ping_write, +}; + +/* -- registration ---------------------------------------------------- */ + +int daedalus_chardev_init(void) +{ + struct daedalus_chardev *dev; + int ret; + + dev = kzalloc(sizeof(*dev), GFP_KERNEL); + if (!dev) + return -ENOMEM; + mutex_init(&dev->open_lock); + mutex_init(&dev->req_lock); + INIT_LIST_HEAD(&dev->req_queue); + init_waitqueue_head(&dev->req_wait); + + dev->misc.minor = MISC_DYNAMIC_MINOR; + dev->misc.name = DAEDALUS_CHARDEV_NAME; + dev->misc.fops = &daedalus_chardev_fops; + dev->misc.mode = 0660; /* root:video, like /dev/videoNN */ + + ret = misc_register(&dev->misc); + if (ret) { + kfree(dev); + return ret; + } + + dev->debugfs_dir = debugfs_create_dir("daedalus_v4l2", NULL); + if (!IS_ERR(dev->debugfs_dir)) + debugfs_create_file("test_ping", 0200, dev->debugfs_dir, + NULL, &daedalus_test_ping_fops); + + g_chardev = dev; + pr_info("daedalus_v4l2: /dev/%s registered\n", DAEDALUS_CHARDEV_NAME); + return 0; +} + +void daedalus_chardev_exit(void) +{ + struct daedalus_chardev *dev = g_chardev; + struct daedalus_chardev_msg *msg; + + if (!dev) + return; + debugfs_remove_recursive(dev->debugfs_dir); + misc_deregister(&dev->misc); + + while ((msg = list_first_entry_or_null(&dev->req_queue, + struct daedalus_chardev_msg, + list)) != NULL) { + list_del(&msg->list); + daedalus_chardev_msg_free(msg); + } + mutex_destroy(&dev->req_lock); + mutex_destroy(&dev->open_lock); + kfree(dev); + g_chardev = NULL; +} diff --git a/kernel/daedalus_v4l2_chardev.h b/kernel/daedalus_v4l2_chardev.h new file mode 100644 index 0000000..3388b2d --- /dev/null +++ b/kernel/daedalus_v4l2_chardev.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * daedalus-v4l2 — chardev bridge interface (kernel-internal). + */ +#ifndef DAEDALUS_V4L2_CHARDEV_H +#define DAEDALUS_V4L2_CHARDEV_H + +#include + +int daedalus_chardev_init(void); +void daedalus_chardev_exit(void); + +/** + * daedalus_chardev_enqueue_req() - queue a request for the daemon + * @type: request type (must have high bit clear; see enum + * daedalus_msg_type in include/daedalus_v4l2_proto.h) + * @cookie: caller-supplied identifier; echoed in matching response + * @payload: pointer to payload bytes (may be NULL if @payload_len = 0) + * @payload_len: payload size in bytes (0..DAEDALUS_PROTO_MAX_PAYLOAD) + * + * Return: 0 on success, -ENODEV if chardev not registered, + * -ENOSPC if the queue is full, -EMSGSIZE if @payload_len is + * too large, -EINVAL if @type has the response bit set, + * -ENOMEM on allocation failure. + */ +int daedalus_chardev_enqueue_req(__u32 type, __u32 cookie, + const void *payload, size_t payload_len); + +#endif /* DAEDALUS_V4L2_CHARDEV_H */ diff --git a/kernel/daedalus_v4l2_main.c b/kernel/daedalus_v4l2_main.c index caab5d2..6e0236a 100644 --- a/kernel/daedalus_v4l2_main.c +++ b/kernel/daedalus_v4l2_main.c @@ -28,6 +28,8 @@ #include #include +#include "daedalus_v4l2_chardev.h" + #define DAEDALUS_DRV_NAME "daedalus_v4l2" #define DAEDALUS_VIDEO_NAME "daedalus" @@ -181,29 +183,40 @@ static int __init daedalus_init(void) { int ret; + ret = daedalus_chardev_init(); + if (ret) + return ret; + daedalus_platform_device = platform_device_alloc(DAEDALUS_DRV_NAME, -1); - if (!daedalus_platform_device) - return -ENOMEM; + if (!daedalus_platform_device) { + ret = -ENOMEM; + goto err_chardev; + } ret = platform_device_add(daedalus_platform_device); if (ret) { platform_device_put(daedalus_platform_device); - return ret; + goto err_chardev; } ret = platform_driver_register(&daedalus_platform_driver); if (ret) { platform_device_unregister(daedalus_platform_device); - return ret; + goto err_chardev; } return 0; + +err_chardev: + daedalus_chardev_exit(); + return ret; } static void __exit daedalus_exit(void) { platform_driver_unregister(&daedalus_platform_driver); platform_device_unregister(daedalus_platform_device); + daedalus_chardev_exit(); } module_init(daedalus_init); diff --git a/tools/Makefile b/tools/Makefile new file mode 100644 index 0000000..6243836 --- /dev/null +++ b/tools/Makefile @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# daedalus-v4l2 — userspace tools for Phase 8.2+ verification. + +CC ?= cc +CFLAGS ?= -Wall -Wextra -O2 +CFLAGS += -I../include + +TOOLS := test_chardev_pingpong + +all: $(TOOLS) + +%: %.c + $(CC) $(CFLAGS) $< -o $@ + +clean: + rm -f $(TOOLS) + +.PHONY: all clean diff --git a/tools/test_chardev_pingpong.c b/tools/test_chardev_pingpong.c new file mode 100644 index 0000000..576d154 --- /dev/null +++ b/tools/test_chardev_pingpong.c @@ -0,0 +1,124 @@ +/* + * SPDX-License-Identifier: BSD-2-Clause + * + * Phase 8.2 verification tool — daemon-side chardev exerciser. + * + * Opens /dev/daedalus-v4l2, validates the chardev round-trip: + * 1. Open: must succeed (-EBUSY if another instance is open). + * 2. Non-blocking read: must return -EAGAIN (empty queue). + * 3. Trigger a kernel-injected PING via the debugfs trigger + * (writes any byte to /sys/kernel/debug/daedalus_v4l2/test_ping). + * 4. Read: should return a full PING message (header + 24-byte + * payload). Magic / version / type / cookie must match. + * 5. Write a matching PONG response. Kernel logs at pr_debug. + * 6. Close. + * + * Build: cc -Wall -Wextra -O2 -I../include test_chardev_pingpong.c + * -o test_chardev_pingpong + */ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "daedalus_v4l2_proto.h" + +#define CHARDEV_PATH "/dev/daedalus-v4l2" +#define DEBUGFS_PING_PATH "/sys/kernel/debug/daedalus_v4l2/test_ping" + +#define die(fmt, ...) do { \ + fprintf(stderr, "FAIL: " fmt ": %s\n", \ + ##__VA_ARGS__, strerror(errno)); \ + exit(1); \ +} while (0) + +static void trigger_ping(void) +{ + int fd = open(DEBUGFS_PING_PATH, O_WRONLY); + if (fd < 0) + die("open(%s) — is the module loaded? " + "is the test_ping debugfs entry world-mounted " + "via /sys/kernel/debug? Try `sudo mount -t debugfs none " + "/sys/kernel/debug`", + DEBUGFS_PING_PATH); + if (write(fd, "1", 1) != 1) + die("write(%s)", DEBUGFS_PING_PATH); + close(fd); +} + +int main(void) +{ + uint8_t buf[1024]; + struct daedalus_msg_hdr *hdr = (struct daedalus_msg_hdr *) buf; + ssize_t n; + int fd; + + printf("opening %s...\n", CHARDEV_PATH); + fd = open(CHARDEV_PATH, O_RDWR | O_NONBLOCK); + if (fd < 0) + die("open(%s)", CHARDEV_PATH); + + /* Step 2: empty queue → non-blocking read returns -EAGAIN. */ + n = read(fd, buf, sizeof(buf)); + if (n != -1 || errno != EAGAIN) + die("expected EAGAIN, got n=%zd errno=%d", n, errno); + printf(" non-blocking read on empty queue: EAGAIN ✓\n"); + + /* Step 3: trigger a kernel-injected PING. */ + trigger_ping(); + printf(" injected PING via debugfs ✓\n"); + + /* Step 4: blocking read should now succeed. */ + int flags = fcntl(fd, F_GETFL); + if (flags < 0 || fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0) + die("fcntl clear O_NONBLOCK"); + + n = read(fd, buf, sizeof(buf)); + if (n < 0) + die("read"); + if ((size_t) n < sizeof(*hdr)) + die("short read: %zd bytes (want >= %zu)", n, sizeof(*hdr)); + if (hdr->magic != DAEDALUS_PROTO_MAGIC) + die("bad magic 0x%08x (want 0x%08x)", + hdr->magic, DAEDALUS_PROTO_MAGIC); + if (hdr->version != DAEDALUS_PROTO_VERSION) + die("bad version %u", hdr->version); + if (hdr->type != DAEDALUS_MSG_PING) + die("got type 0x%08x (want PING 0x%08x)", + hdr->type, DAEDALUS_MSG_PING); + if (hdr->cookie != 0x1234u) + die("got cookie 0x%08x (want 0x1234)", hdr->cookie); + if ((size_t) n != sizeof(*hdr) + hdr->payload_len) + die("payload truncated"); + printf(" read PING: magic ✓ version ✓ type=PING ✓ cookie=0x%x ✓ payload=%u bytes\n", + hdr->cookie, hdr->payload_len); + printf(" payload: \"%.*s\"\n", + (int) hdr->payload_len, + (const char *) (buf + sizeof(*hdr))); + + /* Step 5: write a matching PONG response. Kernel pr_debugs it. */ + struct daedalus_msg_hdr pong = { + .magic = DAEDALUS_PROTO_MAGIC, + .version = DAEDALUS_PROTO_VERSION, + .type = DAEDALUS_MSG_PONG, + .cookie = 0x1234u, + .payload_len = 0, + .reserved = 0, + }; + n = write(fd, &pong, sizeof(pong)); + if (n != (ssize_t) sizeof(pong)) + die("write PONG (n=%zd)", n); + printf(" wrote PONG (cookie=0x%x) ✓\n", pong.cookie); + + close(fd); + printf("\nALL TESTS PASSED.\n"); + printf("(check `sudo dmesg | tail` for the kernel-side\n" + " 'chardev got response' pr_debug line.)\n"); + return 0; +}