94be8c3d03
Fixes issue #146 — daemon-crash (SIGKILL, SEGV, anything that triggers chardev release) leaves V4L2 consumers in unkillable TASK_UNINTERRUPTIBLE on /dev/video0 close. ## Root cause device_run() adds an entry to dev->inflight when it sends a REQ_DECODE to the daemon, marking the m2m job as "running". The job is only cleared via v4l2_m2m_buf_done_and_job_finish() in daedalus_complete_resp_frame(), which only fires on RESP_FRAME. If the daemon dies (SIGKILL, SEGV, exit) BEFORE writing the matching RESP_FRAME: - the inflight entry is never popped - v4l2_m2m_buf_done_and_job_finish is never called - the m2m scheduler still thinks a job is running Later, when the V4L2 consumer's close() runs (or gets signalled to exit), v4l2_m2m_ctx_release() → v4l2_m2m_cancel_job() waits for !job_running indefinitely. The consumer enters D-state and survives SIGKILL until reboot. Reproduced on hertz 2026-05-23, kernel 6.12.75+rpt-rpi-2712: $ sudo kill -STOP $DAEMON_PID # block daemon I/O $ ./test_m2m_decode keyframe.bin out.nv12 1920 1080 vp9 & $ sudo kill -9 $DAEMON_PID # chardev_release fires $ kill -9 $CLIENT_PID # ignored — D-state # client stack: v4l2_m2m_cancel_job+0x14c [v4l2_mem2mem] v4l2_m2m_ctx_release+0x20 [v4l2_mem2mem] daedalus_release+0x2c [daedalus_v4l2] v4l2_release+0x7c [videodev] __fput → do_exit → SIGKILL never delivered ## Fix New API daedalus_drain_inflight_on_disconnect() in main.{c,h}: walks the in-flight list, marks both src+dst buffers VB2_BUF_STATE_ERROR via v4l2_m2m_buf_done_and_job_finish(), and releases the bound media_request if any. Same completion shape as daedalus_complete_resp_frame() takes on the success path, just with state = ERROR for every in-flight entry. chardev_release calls the drain after flushing dev->req_queue (messages still in req_queue weren't released to the daemon yet, so they don't need the m2m-job-finish dance — freeing them is sufficient). The order matters: queue first (cheap), then m2m drain (heavier, takes the inflight list). Locking: list_splice_init under inflight_lock to take the entire list atomically; lock dropped before iterating because v4l2_m2m_buf_done_and_job_finish can sleep via vb2's buffer-done dispatch and can re-enter device_run via the scheduler (which would need inflight_lock again on the next REQ_DECODE). ## Verification path Cannot rmmod the running module on hertz right now — the D-state corpse from the repro session pins the refcount. Verification of the fixed module needs a reboot or fresh test host: $ sudo reboot # clears hung client $ sudo make modules_install # install new .ko $ sudo modprobe daedalus_v4l2 $ # rerun the repro script — client should die cleanly with $ # an -EIO / similar return from poll/DQBUF instead of hanging. Build: clean on Linux 6.12.75 + rpt-rpi-2712, no new warnings. The pre-existing "frame size 2128 > 2048" warning on daedalus_device_run is unchanged by this commit. ## Followup not in scope If a new V4L2 consumer races a REQ_DECODE through device_run AFTER the drain has spliced the list (but before the daemon chardev is reopened), the new entry sits in a freshly-empty inflight list and the same hang can recur for that consumer when the systemd auto-restart of the daemon either fails or takes longer than the consumer's patience. A secondary safeguard would be to fail-fast in device_run when dev->chardev is unopened — proposing as a separate ticket if this race materialises in practice. Closes #146.
130 lines
4.9 KiB
C
130 lines
4.9 KiB
C
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
/*
|
|
* daedalus-v4l2 — kernel-internal device/state declarations.
|
|
*
|
|
* Shared between daedalus_v4l2_main.c (V4L2 m2m driver) and
|
|
* daedalus_v4l2_chardev.c (kernel↔daemon bridge). The chardev
|
|
* needs to look up in-flight V4L2 requests by cookie to complete
|
|
* the m2m job when RESP_FRAME arrives — that path lives in
|
|
* daedalus_complete_resp_frame().
|
|
*/
|
|
#ifndef DAEDALUS_V4L2_MAIN_H
|
|
#define DAEDALUS_V4L2_MAIN_H
|
|
|
|
#include <linux/list.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/platform_device.h>
|
|
|
|
#include <media/v4l2-device.h>
|
|
#include <media/v4l2-dev.h>
|
|
#include <media/v4l2-mem2mem.h>
|
|
#include <media/media-device.h>
|
|
|
|
#include "daedalus_v4l2_proto.h"
|
|
|
|
/**
|
|
* struct daedalus_dev - top-level device state (singleton for now)
|
|
* @pdev: owning platform device (synthesised in module_init)
|
|
* @v4l2_dev: V4L2 device parent for any video_device we register
|
|
* @vdev: video_device exposed as /dev/videoNN
|
|
* @m2m_dev: mem2mem device shared by all per-open contexts
|
|
* @m2m_lock: serialises vb2 queue + v4l2 ioctl ops
|
|
* @inflight: list of struct daedalus_inflight (REQ_DECODE sent,
|
|
* RESP_FRAME not yet returned)
|
|
* @inflight_lock: protects @inflight
|
|
*
|
|
* Singleton per-module instance. Multi-instance support (one
|
|
* decoder per /dev/videoNN) would require breaking g_daedalus_dev
|
|
* out of daedalus_v4l2_main.c; not needed yet.
|
|
*/
|
|
struct daedalus_dev {
|
|
struct platform_device *pdev;
|
|
struct v4l2_device v4l2_dev;
|
|
struct video_device vdev;
|
|
struct v4l2_m2m_dev *m2m_dev;
|
|
struct media_device mdev;
|
|
struct mutex m2m_lock;
|
|
struct list_head inflight;
|
|
struct mutex inflight_lock;
|
|
};
|
|
|
|
/* Module-wide singleton accessor (chardev needs this for RESP_FRAME). */
|
|
struct daedalus_dev *daedalus_get_dev(void);
|
|
|
|
/**
|
|
* daedalus_next_cookie() - shared cookie allocator
|
|
*
|
|
* Returns the next monotonically increasing request cookie.
|
|
* Used by both the V4L2 m2m device_run path (for REQ_DECODE
|
|
* from real OUTPUT buffers) and the chardev debugfs
|
|
* test_decode path (for hand-crafted REQ_DECODE injection),
|
|
* so the two namespaces never collide and RESP_FRAME logs
|
|
* stay deterministic.
|
|
*/
|
|
u32 daedalus_next_cookie(void);
|
|
|
|
/**
|
|
* daedalus_complete_resp_frame() - chardev RESP_FRAME completion
|
|
* @cookie: cookie carried by the matching REQ_DECODE
|
|
* @fr: RESP_FRAME header from the daemon
|
|
* @pixels: inline pixel bytes following the header in the
|
|
* chardev payload (may be NULL if @pixels_len == 0)
|
|
* @pixels_len: number of inline pixel bytes
|
|
*
|
|
* Called from the chardev write() path on RESP_FRAME. Looks up
|
|
* the in-flight request, copies inline pixel data into the
|
|
* CAPTURE vb2 buffer if available (Phase 8.5 path; Phase 8.6
|
|
* skips the copy because the daemon decoded directly into the
|
|
* dmabuf), then completes both src+dst buffers and finishes
|
|
* the m2m job. Silently drops responses for unknown cookies
|
|
* (pr_warn_ratelimited).
|
|
*/
|
|
void daedalus_complete_resp_frame(u32 cookie,
|
|
const struct daedalus_resp_frame *fr,
|
|
const u8 *pixels, size_t pixels_len);
|
|
|
|
/**
|
|
* daedalus_export_capture_dmabuf() - chardev GET_DMABUF backend
|
|
* @cookie: cookie from the matching REQ_DECODE
|
|
* @plane: plane index (0-based) within the CAPTURE buffer
|
|
* @flags: flags for dma_buf_fd (O_CLOEXEC etc.)
|
|
* @out_fd: out: installed dmabuf fd in the calling task's
|
|
* fd table (only valid when return value == 0)
|
|
*
|
|
* Called from the chardev DAEDALUS_IOC_GET_DMABUF ioctl
|
|
* handler. Looks up the in-flight V4L2 request by cookie,
|
|
* exports the CAPTURE vb2 buffer's plane as a dma_buf via
|
|
* vb2_core_expbuf in the daemon's task context. Caller must
|
|
* NOT touch out_fd on non-zero return.
|
|
*
|
|
* Return: 0 on success, -EINVAL for unknown cookie or bad
|
|
* plane, propagated -errno from vb2_core_expbuf otherwise.
|
|
*/
|
|
int daedalus_export_capture_dmabuf(u32 cookie, u32 plane, u32 flags,
|
|
int *out_fd);
|
|
|
|
/**
|
|
* daedalus_drain_inflight_on_disconnect() - fail all in-flight m2m jobs
|
|
*
|
|
* Called from daedalus_chardev_release() when the daemon disconnects
|
|
* (graceful close, SIGKILL, daemon crash — anything that triggers
|
|
* chardev release). Walks the in-flight list and, for every entry,
|
|
* marks both src+dst buffers VB2_BUF_STATE_ERROR and calls
|
|
* v4l2_m2m_buf_done_and_job_finish() to clear the m2m scheduler's
|
|
* "job_running" flag.
|
|
*
|
|
* Without this, v4l2_m2m_cancel_job() (called from
|
|
* v4l2_m2m_ctx_release() during the consumer's close() / task exit)
|
|
* blocks forever waiting for a job_finish that the dead daemon will
|
|
* never send — the consumer enters TASK_UNINTERRUPTIBLE and survives
|
|
* SIGKILL until reboot. See issue #146 for the full trace.
|
|
*
|
|
* Safe to call with an empty in-flight list; no-op in that case.
|
|
* Must NOT be called from atomic context — uses inflight_lock
|
|
* (sleeping mutex) and v4l2_m2m_buf_done_and_job_finish (which can
|
|
* sleep via vb2 buffer-done dispatch).
|
|
*/
|
|
void daedalus_drain_inflight_on_disconnect(void);
|
|
|
|
#endif /* DAEDALUS_V4L2_MAIN_H */
|