RE: [PATCH v32 5/7] firmware: imx: adds miscdev

"Pankaj Gupta (OSS)" <[email protected]> Thu, 30 Jul 2026 10:45:07 +0000
Newsgroups dev.linux.lists.imx,dev.linux.lists.sashiko-reviews,org.kernel.vger.linux-devicetree
Message-ID <GV2PR04MB12271F3A8F05135C8E2ABA74F95C92@GV2PR04MB12271.eurprd04.prod.outlook.com>
Thanks for the thorough review of v32 5/7. Eight [High] comments; my
disposition is below. Five point at issues and are FIXED in the
follow-up commit; three are NO-CHANGE (two intended-by-design, one false
positive). The three NO-CHANGE items - and in particular the -EINTR one
that has been raised on several revisions - are explained in full so the
rationale is on the record and does not need to be re-litigated each round.

> - [High] Implicit padding in `struct se_ioctl_cmd_snd_rcv_rsp_info` causes an
> ABI mismatch between 32-bit userspace and 64-bit kernel.
Fixed.

> - [High] The driver returns -EBUSY when file operations are interrupted by a
> signal, violating standard POSIX syscall semantics.
Fixed.

> - [High] If a signal interrupts `se_if_fops_read()` after it successfully receives a
> message, the message is stranded and the firmware protocol permanently
> deadlocks.
Fixed.

> - [High] A successful firmware transaction is reported to userspace as
> interrupted (`-EINTR`) if a signal is pending, discarding the successful response
> payload.
No Changes. By Design.

> - [High] A TOCTOU race between `ele_msg_send_rcv` and
> `se_if_probe_cleanup` causes a lost wakeup, resulting in a 1.3-hour unbind
> hang.
Fixed.

> - [High] Kernel crash in the error path of `init_misc_device_context` due to
> accessing an uninitialized `dev_ctx->priv` pointer.
Fixed.

> - [High] Unbounded DMA memory allocation in the device `open` path allows
> unprivileged users to exhaust contiguous DMA memory.
No change. Bounded pool.

> - [High] The dynamically allocated `se_if_open_gate` structure can be accessed
> after it is freed if a concurrent `open()` races with device teardown.
Not an issue. False positive.


> > +++ b/drivers/firmware/imx/ele_common.c
> [ ... ]
> > @@ -188,10 +258,21 @@ int ele_msg_send_rcv(struct se_if_device_ctx
> > *dev_ctx, void *tx_msg,
> >
> >  	guard(mutex)(&priv->se_if_cmd_lock);
> >
> > +	/*
> > +	 * Teardown has begun: do not arm a new transaction. A thread that
> was
> > +	 * blocked on se_if_cmd_lock while se_if_probe_cleanup() ran its
> > +	 * complete_all() would otherwise miss that wake-up, re-arm the
> > +	 * completion below and wait the full timeout, while unbind blocks
> > +	 * indefinitely on this thread's fops_lock. Bail out instead.
> > +	 */
> > +	if (atomic_read(&priv->going_away))
> > +		return -ENODEV;
> > +
> >  	if (atomic_read(&priv->fw_busy)) {
> >  		dev_dbg(priv->dev, "%s: ELE became unresponsive.\n",
> dev_ctx->devname);
> >  		return -EBUSY;
> >  	}
> > +
> >  	reinit_completion(&priv->waiting_rsp_clbk_hdl.done);
> 
> [Severity: High]
> Could this lead to a TOCTOU race and a lost wakeup during unbind?
> 
> In ele_msg_send_rcv(), atomic_read(&priv->going_away) is checked locklessly
> before calling reinit_completion(). Concurrently, se_if_probe_cleanup() sets
> going_away to 1 and calls complete_all() to forcefully wake any waiters:
> 
> se_if_probe_cleanup() {
>     atomic_set(&priv->going_away, 1);
>     ...
>     complete_all(&priv->waiting_rsp_clbk_hdl.done);
> }
> 
> Since se_if_probe_cleanup() does not acquire se_if_cmd_lock, it can execute
> immediately after ele_msg_send_rcv() checks going_away. The
> reinit_completion() call here would then overwrite the forced wakeup. This
> leaves ele_msg_rcv() waiting indefinitely, which can cause the device unbind
> process to hang.
> 
Correct - the going_away check, reinit_completion() and the arming all run
under se_if_cmd_lock in ele_msg_send_rcv(), but teardown was setting
going_away and calling complete_all() OUTSIDE that lock, so a waiter parked
on se_if_cmd_lock could arm the completion after the teardown wakeup already
fired and then block for the full timeout while unbind waited on its
fops_lock. Fixed by taking the same lock in teardown:

	scoped_guard(mutex, &priv->se_if_cmd_lock) {
		atomic_set(&priv->going_away, 1);
		complete_all(&priv->waiting_rsp_clbk_hdl.done);
	}

Now teardown and the arming path are mutually exclusive: a thread either
observes going_away and bails with -ENODEV before arming, or it armed first
and complete_all() is guaranteed to reach it. The lockless
atomic_read(&going_away) fast-path in ele_msg_send_rcv() is retained only as
an early-out; correctness comes from the lock, so it is no longer a TOCTOU.

> >  	/* Publish rx_msg/rx_msg_sz under the lock read by
> se_if_rx_callback(). */
> >  	spin_lock_irqsave(&priv->waiting_rsp_clbk_hdl.clbk_rx_lock, flags);
> [ ... ]
> > diff --git a/drivers/firmware/imx/se_ctrl.c
> > b/drivers/firmware/imx/se_ctrl.c index a8974eef190b6..8698d77172465
> > 100644
> > --- a/drivers/firmware/imx/se_ctrl.c
> > +++ b/drivers/firmware/imx/se_ctrl.c
> [ ... ]
> > @@ -302,13 +618,51 @@ static int init_misc_device_context(struct se_if_priv
> *priv, int ch_id,
> >  	if (!dev_ctx->devname)
> >  		goto exit;
> >
> > +	mutex_init(&dev_ctx->fops_lock);
> > +	lockdep_set_class(&dev_ctx->fops_lock, &se_priv_ctx_fops_key);
> > +
> >  	dev_ctx->priv = priv;
> > +	kref_init(&dev_ctx->refcount);
> > +	dev_ctx->cleanup_done = false;
> >  	*new_dev_ctx = dev_ctx;
> > +	set_se_rcv_msg_timeout(dev_ctx,
> SE_RCV_MSG_DEFAULT_TIMEOUT_MS);
> > +
> > +	ret = init_se_shared_mem(dev_ctx);
> > +	if (ret < 0)
> > +		goto exit;
> > +
> > +	gate = kzalloc_obj(*gate, GFP_KERNEL);
> > +	if (!gate) {
> > +		ret = -ENOMEM;
> > +		goto exit;
> > +	}
> > +
> > +	mutex_init(&gate->lock);
> > +	kref_init(&gate->refcount);    /* device-owned reference */
> > +	gate->priv = priv;
> > +	gate->dying = false;
> > +	priv->open_gate = gate;
> > +
> > +	/*
> > +	 * The miscdevice storage is now owned by the open gate object.
> > +	 * priv->priv_dev_ctx still keeps a pointer to that miscdevice.
> > +	 */
> > +	dev_ctx->miscdev = &gate->miscdev;
> > +
> > +	dev_ctx->miscdev->name = dev_ctx->devname;
> > +	dev_ctx->miscdev->minor = MISC_DYNAMIC_MINOR;
> > +	dev_ctx->miscdev->fops = se_if_fops;
> > +	dev_ctx->miscdev->parent = priv->dev;
> >
> >  	return 0;
> >  exit:
> >  	*new_dev_ctx = NULL;
> >
> > +	if (gate) {
> > +		priv->open_gate = NULL;
> > +		se_if_open_gate_put(gate);
> > +	}
> > +	cleanup_se_shared_mem(dev_ctx, true);
> 
> [Severity: High]
> Can this error path trigger a NULL pointer dereference?
> 
> If kasprintf() fails to allocate memory for dev_ctx->devname earlier in
> init_misc_device_context(), the code jumps to the exit label. At this point,
> dev_ctx->priv has not yet been assigned and is NULL.
> 
> This error path calls cleanup_se_shared_mem(dev_ctx, true), which does:
> 
> cleanup_se_shared_mem() {
>     struct se_if_priv *priv = dev_ctx->priv;
>     ...
>     if (priv->mem_pool)
>         ...
> }
> 
> This will unconditionally dereference the uninitialized priv pointer, causing an
> immediate kernel panic.
> 
Agreed. dev_ctx->priv = priv is moved up to immediately after the dev_ctx
allocation succeeds, before any goto to the exit/cleanup path, so
cleanup_se_shared_mem() -> priv->mem_pool is always valid even when
kasprintf() fails:

	if (!dev_ctx)
		return ret;
	dev_ctx->priv = priv;          /* set before any goto exit */
	dev_ctx->devname = kasprintf(...);


> >  	kfree(dev_ctx->devname);
> >  	kfree(dev_ctx);
> >  	return dev_err_probe(priv->dev, ret, "%s", err_str);
> [ ... ]
> > +static int se_ioctl_cmd_snd_rcv_rsp_handler(struct se_if_device_ctx
> *dev_ctx,
> > +					    void __user *uarg)
> > +{
> [ ... ]
> > +	err = ele_msg_send_rcv(dev_ctx, tx_msg,
> cmd_snd_rcv_rsp_info.tx_buf_sz,
> > +			       rx_msg, cmd_snd_rcv_rsp_info.rx_buf_sz);
> > +	if (err < 0) {
> > +		se_ioctl_cmd_snd_rcv_cleanup(dev_ctx, uarg,
> &cmd_snd_rcv_rsp_info);
> > +
> > +		/*
> > +		 * -ERESTARTSYS here means the wait was interrupted by a
> signal
> > +		 * after the command had already been handed to (and
> possibly
> > +		 * executed by) the firmware. Returning -ERESTARTSYS lets the
> VFS
> > +		 * transparently restart the ioctl, which would re-run the
> command
> > +		 * with the just cleaned-up (zeroed) shared input buffers.
> Report
> > +		 * -EINTR instead so the syscall is not auto-restarted;
> userspace
> > +		 * can decide whether to reissue it.
> > +		 */
> > +		if (err == -ERESTARTSYS)
> > +			err = -EINTR;
> 
> [Severity: High]
> Is it safe to discard a successful firmware response when a signal is pending?
> 
> If a successful firmware transaction was interrupted by a signal, the lower
> layers might overwrite the success return code with -ERESTARTSYS (which is
> translated to -EINTR here). When this happens, the successful response
> payload is discarded in se_ioctl_cmd_snd_rcv_cleanup().
> 
> Because the kernel successfully retrieved the response and the firmware has
> committed the action, returning -EINTR breaks idempotency for state-mutating
> commands. Userspace receives -EINTR and might unsafely retry commands
> that were actually successfully executed by the firmware. Should the kernel
> return the successful response to userspace instead?
> 
This is deliberate and correct for this interface; it is not an oversight,
and I would like to close it out permanently. Please treat the following as
the authoritative rationale.

What actually happens (there is no lost data and no torn state):

  1. The command is atomic at the firmware. By the time control reaches the
     ioctl handler the enclave has either fully executed the command or not
     executed it at all; there is no half-applied state to reconcile.

  2. The signal is NOT acted on mid-transaction. When a signal arrives
     during the wait, ele_msg_rcv() does NOT bail out. It records the signal
     (WRITE_ONCE(signal_rcvd, true)) and *keeps waiting uninterruptibly*
     until the firmware response actually arrives and the MU protocol is
     resynchronised (ele_common.c ~ line 150-160). Only after the
     transaction is fully drained does ele_msg_send_rcv() convert the
     already-successful wait into -ERESTARTSYS (ele_common.c ~ line 306-314).
     So the protocol is never left mid-flight and the next command is not
     corrupted - the whole point of the deferred-signal design.

  3. The conversion to -EINTR is the safety property, not a bug. In
     se_ioctl_cmd_snd_rcv_rsp_handler() the only reason we downgrade
     -ERESTARTSYS to -EINTR is to STOP the VFS from transparently
     auto-restarting the ioctl. Auto-restart would re-issue a non-idempotent,
     state-mutating firmware command a second time (e.g. a monotonic
     counter, key generation/rotation, provisioning or lifecycle step) with
     no userspace involvement. For this device a silent second execution is
     the actual dangerous behaviour; -EINTR is what prevents it:

	/*
	 * -ERESTARTSYS here means the wait was interrupted by a signal
	 * after the command had already been handed to (and possibly
	 * executed by) the firmware. Returning -ERESTARTSYS lets the VFS
	 * transparently restart the ioctl, which would re-run the command
	 * ... Report -EINTR instead so the syscall is not auto-restarted;
	 * userspace can decide whether to reissue it.
	 */
	if (err == -ERESTARTSYS)
		err = -EINTR;

  4. "Discarding the successful response payload" is the intended
     userspace contract, not data loss in the kernel. The semantics are:
     if you send this command and then Ctrl+C (or otherwise take a signal)
     before it completes, the syscall reports -EINTR and you must NOT assume
     anything about the result buffer. Userspace decides whether to reissue
     - which is exactly right for a non-idempotent command, because only the
     application knows whether re-running is safe. The kernel deliberately
     does not make that decision on the user's behalf by returning success.

Why not "return the success/response instead", as the comment suggests:
  - If we returned 0/size on an interrupted call, a Ctrl+C that the user
    intends as "abort" would instead silently succeed, and the application
    would have no way to distinguish "completed" from "completed but I asked
    to stop". That hides the interruption, which POSIX explicitly allows a
    driver to surface as -EINTR for a slow device operation.
  - Reporting -EINTR after a side effect has already occurred is standard,
    accepted kernel behaviour for slow/uninterruptible-side-effect syscalls
    (write() to a slow device can return a short count or -EINTR after bytes
    have been committed; many ioctl-driven devices behave the same). -EINTR
    does not promise "nothing happened" - it promises "the call was
    interrupted, re-evaluate". The non-idempotency of the firmware command
    is precisely why we must not auto-restart.

Net: the response is fully received and the protocol is left clean; the
-EINTR is a deliberate "do not auto-restart a non-idempotent command"
signal to userspace, and the existing in-code comment documents it. No code
change. I would ask that this specific pattern (deferred signal + downgrade
to -EINTR to suppress auto-restart on a non-idempotent firmware command) be
suppressed for this file rather than re-reported.
> > +
> > +		return err;
> > +	}
> [ ... ]
> > +/* Write a message to the MU. */
> > +static ssize_t se_if_fops_write(struct file *fp, const char __user *buf,
> > +				size_t size, loff_t *ppos)
> > +{
> > +	struct se_if_device_ctx *dev_ctx = fp->private_data;
> > +	struct se_if_priv *priv;
> > +	int err;
> > +
> > +	scoped_cond_guard(mutex_intr, return -EBUSY, &dev_ctx->fops_lock) {
> 
> [Severity: High]
> Should this return -ERESTARTSYS instead of -EBUSY when interrupted?
> 
> When the interruptible lock acquisition fails due to a pending signal, masking
> the error with -EBUSY breaks the standard POSIX syscall restart mechanism.
> Userspace receives a hard error (EBUSY) instead of an interrupt (EINTR), which
> prevents applications from properly handling signals or transparently retrying
> the syscall.
> 

Agreed. mutex_intr / mutex_lock_interruptible() aborting on a pending
signal is an interrupted-wait, not device contention. All such paths now
return -ERESTARTSYS instead of -EBUSY:
  - se_if_fops_write()  : scoped_cond_guard(mutex_intr, return -ERESTARTSYS, ...)
  - se_if_fops_read()   : scoped_cond_guard(mutex_intr, return -ERESTARTSYS, ...)
  - se_ioctl()          : scoped_cond_guard(mutex_intr, return -ERESTARTSYS, ...)
  - se_if_fops_open()   : both the gate->lock and priv_dev_ctx->fops_lock
                          acquisitions now yield -ERESTARTSYS

The kernel then restarts the syscall or reports -EINTR per the caller's
SA_RESTART disposition. The three remaining -EBUSY returns are genuine
non-signal conditions (command receiver already registered; the two
firmware-busy circuit-breaker checks) and are intentionally left as -EBUSY.

> > +		if (dev_ctx->cleanup_done)
> > +			return -ENODEV;
> [ ... ]
> > +/*
> > + * Read a message from the MU.
> > + * Blocking until a message is available.
> > + */
> > +static ssize_t se_if_fops_read(struct file *fp, char __user *buf, size_t size,
> > +			       loff_t *ppos)
> > +{
> > +	struct se_if_device_ctx *dev_ctx = fp->private_data;
> > +	u8 rx_msg_snap[MAX_NVM_MSG_LEN];
> > +	struct se_if_priv *priv;
> > +	unsigned long flags;
> > +	size_t copy_len;
> > +	int err;
> > +
> > +	scoped_cond_guard(mutex_intr, return -EBUSY, &dev_ctx->fops_lock) {
> > +		priv = dev_ctx->priv;
> [ ... ]
> > +	err = ele_msg_rcv(dev_ctx, &priv->cmd_receiver_clbk_hdl);
> > +	if (err < 0) {
> > +		if (err != -ERESTARTSYS)
> > +			dev_err(priv->dev,
> > +				"%s: Er[0x%x]: Signal Interrupted. Current act-
> dev-ctx count: %d.",
> > +				dev_ctx->devname, err, dev_ctx->priv-
> >active_devctx_count);
> > +		return err;
> > +	}
> > +
> > +	/*
> > +	 * Reacquire fops_lock before touching any dev_ctx state (pending
> lists,
> > +	 * rx_msg) after the blocking wait. fops_lock was dropped before
> calling
> > +	 * ele_msg_rcv(). If cleanup_dev_ctx() ran concurrently it could have
> > +	 * freed the DMA buffers and the pending lists, leading to UAF and list
> > +	 * corruption. Re-checking cleanup_done under fops_lock prevents
> that.
> > +	 */
> > +	if (mutex_lock_interruptible(&dev_ctx->fops_lock))
> > +		return -ERESTARTSYS;
> 
> [Severity: High]
> Does this risk permanently stranding the message if a signal arrives?
> 
> In se_if_fops_read(), the driver blocks in ele_msg_rcv() waiting for a message
> from the firmware. When a message arrives, the completion is consumed and
> the message is staged in the driver's internal buffer.
> 
> The driver then attempts to reacquire the fops_lock here using an interruptible
> primitive. If a signal is pending at this exact moment, the lock acquisition fails
> and returns -ERESTARTSYS.
> 
> Because the completion has already been consumed, the message is left
> stranded in the buffer. When userspace retries the read() syscall, it will block
> on ele_msg_rcv() again waiting for a new completion. Because the firmware
> protocol is half-duplex, it will wait for userspace to process the already
> delivered message, causing a permanent deadlock.

Agreed, and this is the important one on the read path. Once ele_msg_rcv()
returns success the response has already been consumed from the completion
and staged, so aborting the fops_lock reacquire on a signal would drop a
message that can never be re-fetched on a half-duplex link. The reacquire
is now uninterruptible:

	-	if (mutex_lock_interruptible(&dev_ctx->fops_lock))
	-		return -ERESTARTSYS;
	+	mutex_lock(&dev_ctx->fops_lock);

The blocking wait itself (ele_msg_rcv()) stays interruptible; only the
short, post-receive reacquire that hands the staged message to userspace is
uninterruptible, and the cleanup_done re-check under fops_lock is preserved.

> 
> > +
> > +	if (dev_ctx->cleanup_done) {
> > +		mutex_unlock(&dev_ctx->fops_lock);
> > +		return -ENODEV;
> > +	}
> [ ... ]
> > +/* Open a character device. */
> > +static int se_if_fops_open(struct inode *nd, struct file *fp) {
> > +	struct miscdevice *miscdev = fp->private_data;
> > +	struct se_if_open_gate *gate;
> > +	struct se_if_device_ctx *misc_dev_ctx;
> > +	struct se_if_device_ctx *dev_ctx;
> > +	struct se_if_priv *priv;
> > +	int err = 0;
> > +
> > +	gate = container_of(miscdev, struct se_if_open_gate, miscdev);
> > +
> > +	if (!se_if_open_gate_get(gate))
> 
> [Severity: High]
> Could this lead to a Use-After-Free if a concurrent open races with device
> teardown?
> 
> During device unbind, se_if_probe_cleanup() calls misc_deregister(). While
> misc_deregister() removes the device from the active misc device list, it does
> not prevent a racing VFS open (which already found the miscdev but was
> preempted before calling fops->open) from continuing.
> 
> If unbind completes, se_if_priv_release() drops the final reference to the
> open_gate structure, freeing it. When the delayed open call resumes here, it
> calculates the gate pointer using container_of() and calls
> se_if_open_gate_get(gate). This reads from the freed gate->refcount, causing a
> Use-After-Free.
> 
> > +		return -ENODEV;
> > +
> > +	if (mutex_lock_interruptible(&gate->lock)) {
> > +		se_if_open_gate_put(gate);
> > +		return -EBUSY;
> > +	}
> [ ... ]
> > +	priv->dev_ctx_mono_count++;
> > +	err = init_device_context(priv, priv->dev_ctx_mono_count, &dev_ctx);
> 
The open gate was introduced precisely to make this race safe, so the freed
read cannot happen:
  - misc_open() and misc_deregister() are serialised by the misc core's
    misc_mtx, so a fops->open() cannot begin after deregister has removed
    the node.
  - The gate is a separately kref'd object; the device holds one reference
    for its whole lifetime and only drops it in teardown. se_if_fops_open()
    takes its reference with kref_get_unless_zero() - if teardown has
    already started dropping the last reference, the get fails and open()
    returns -ENODEV rather than touching a dying object.
  - After a successful get, open() re-validates gate->dying / gate->priv
    under gate->lock before using priv, so a concurrently-torn-down gate is
    detected and rejected.

The object read by container_of()/kref_get_unless_zero() is the gate, whose
storage is not freed until its own kref hits zero, which cannot occur while
this path holds (or is about to fail to obtain) a reference. No UAF; no code
change.


> [Severity: High]
> Is there a limit on how much DMA memory can be allocated through this open
> path?
> 
> When a user process opens the device file, se_if_fops_open() invokes
> init_device_context(), which calls init_se_shared_mem(). This unconditionally
> allocates 128KB (MAX_DATA_SIZE_PER_USER) of DMA coherent memory using
> dma_alloc_coherent() per device context.
> 
> Since there is no global limit on the number of dev_ctx instances, a single
> process could rapidly open the device repeatedly. This could instantly
> consume hundreds of megabytes of contiguous DMA memory, which can
> exhaust the pool and prevent other drivers from functioning, leading to a
> Denial of Service.
> 

The premise that this can exhaust general/contiguous DMA memory does not
hold on this device. In probe the device is bound to a fixed, no-map
reserved-memory region (the ele_reserved "shared-dma-pool") via
of_reserved_mem_device_init(). With dev->dma_mem set, dma_alloc_coherent()
is served *exclusively* from that bounded pool - it never falls back to the
system CMA/contiguous allocator, so it cannot starve other drivers.

When the pool is full, dma_alloc_coherent() simply returns NULL,
init_se_shared_mem() returns -ENOMEM, and open() fails gracefully with
-ENOMEM for the offending process; already-open contexts are unaffected.
The effective ceiling is therefore (pool size / MAX_DATA_SIZE_PER_USER),
self-adjusting to the DT-provisioned region, and is further bounded per
process by RLIMIT_NOFILE.

We deliberately do NOT add a hardcoded open-count cap: any fixed number
would either be below what the reserved pool can satisfy (needlessly
rejecting valid users) or above it (dead code, since -ENOMEM triggers
first). The bounded pool is the correct and authoritative limit. No code
change. (If a per-container/cgroup policy is ever wanted that is a separate
DMA-accounting discussion, not a fix for this commit.)

> > +	if (err) {
> > +		dev_err(priv->dev, "Failed[0x%x] to create dev-ctx.", err);
> > +		goto out_unlock_misc;
> > +	}
> [ ... ]
> > diff --git a/include/uapi/linux/se_ioctl.h
> > b/include/uapi/linux/se_ioctl.h new file mode 100644 index
> > 0000000000000..ea14cec05020d
> > --- /dev/null
> > +++ b/include/uapi/linux/se_ioctl.h
> > @@ -0,0 +1,97 @@
> [ ... ]
> > +struct se_ioctl_cmd_snd_rcv_rsp_info {
> > +	__u64 tx_buf;
> > +	__u32 tx_buf_sz;
> > +	__u64 rx_buf;
> > +	__u32 rx_buf_sz;
> > +};
> 
> [Severity: High]
> Does this structure introduce an ABI mismatch between 32-bit userspace and
> a 64-bit kernel?
> 
> The se_ioctl_cmd_snd_rcv_rsp_info struct places a __u64 immediately after a
> __u32. On 64-bit architectures, this induces 4 bytes of padding before the
> __u64 and at the end of the struct, resulting in a 32-byte struct. On some 32-
> bit architectures, __u64 requires only 4-byte alignment, producing a 24-byte
> struct.
> 
> Because the SE_IOCTL_CMD_SEND_RCV_RSP macro incorporates the struct's
> size, the generated IOCTL number will differ between a 32-bit application and
> the 64-bit kernel. When compat_ptr_ioctl passes the 32-bit IOCTL command to
> se_ioctl(), it will fail to match the 64-bit switch case, causing a -ENOTTY error
> for 32-bit compat userspace applications.
> 
> > +
> > +struct se_ioctl_get_soc_info {
> > +	__u16 soc_id;
> > +	__u16 soc_rev;
> > +};
> 
Agreed. The old order (tx_buf __u64, tx_buf_sz __u32, rx_buf __u64,
rx_buf_sz __u32) forces implicit padding before the second __u64, so
sizeof() - and therefore the size encoded into _IOWR() - is not identical
across 32- and 64-bit ABIs. Reordered so both __u64 come first:

	struct se_ioctl_cmd_snd_rcv_rsp_info {
		__u64 tx_buf;
		__u64 rx_buf;
		__u32 tx_buf_sz;
		__u32 rx_buf_sz;
	};

---------------------------------------------------------------------------
Code changes for comments 1,2,3,5,6 are in the follow-up commit
"firmware: imx: harden miscdev signal, ABI and teardown handling"; comments
4, 7 and 8 are NO-CHANGE for the reasons above. Thanks again for the review.