Devicetree
 help / color / mirror / Atom feed
* [PATCH v6 0/2] media: i2c: add Aptina MT9M113 image sensor driver
@ 2026-06-06 19:01 Herman van Hazendonk via B4 Relay
  2026-06-06 19:01 ` [PATCH v6 1/2] dt-bindings: media: i2c: add aptina,mt9m113 Herman van Hazendonk via B4 Relay
  2026-06-06 19:01 ` [PATCH v6 2/2] media: i2c: add Aptina MT9M113 1.3 Mpx SoC sensor driver Herman van Hazendonk via B4 Relay
  0 siblings, 2 replies; 4+ messages in thread
From: Herman van Hazendonk via B4 Relay @ 2026-06-06 19:01 UTC (permalink / raw)
  To: Mauro Carvalho Chehab, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Sakari Ailus
  Cc: linux-media, devicetree, linux-kernel, Herman van Hazendonk

Add a V4L2 subdev driver for the Aptina (now ON Semiconductor) MT9M113
1.3 megapixel SoC image sensor with embedded ISP, as used on the HP
TouchPad (apq8060) front camera. The sensor is programmed over I2C and
streams YUV / RGB / monochrome over a 1-lane MIPI CSI-2 D-PHY link.

v6 addresses six Sashiko Gemini 3.1 findings raised on the v5 patchset,
all of which are real bugs introduced (or exposed) by the v3-r2 release-
callback restructure. One of them — the use-after-free in mt9m113_remove()
during sysfs unbind — was reproduced on hardware as a NULL-pointer
dereference in regulator_bulk_disable() and is fixed by the v6 reorder.

Signed-off-by: Herman van Hazendonk <github.com@herrie.org>
---
Changes in v6:

  - [High] mt9m113_start_streaming() failure path: drop the
    pm_runtime_set_suspended() call. It fails with -EAGAIN while
    runtime PM is still enabled, leaving the PM core in RPM_ACTIVE
    while the chip is in fact off; the next autosuspend would then
    fire mt9m113_runtime_suspend() and call mt9m113_power_off() a
    second time, underflowing both the clk prepare/enable refcount
    and the regulator enable count. Track the chip-off state via a
    new 'bool chip_off' on the sensor; mt9m113_runtime_suspend()
    consumes it to skip its own power_off when start_streaming has
    already collapsed the rail, and mt9m113_runtime_resume() clears
    it after a successful power_on().

  - [High] mt9m113_s_ctrl(): pm_runtime_get_if_in_use() return-code
    check changed from "pm_ret == 0" to "pm_ret <= 0". The old test
    misses any negative return -- in particular -EINVAL after
    pm_runtime_disable() has been called from mt9m113_remove(). With
    a still-open /dev/v4l-subdev* file descriptor, a control set
    issued after unbind would slip past the gate and dereference the
    devm-managed regmap that the driver core has already freed.

  - [High] mt9m113_probe(): early-error paths after kzalloc(sensor)
    but before error_ep_free now goto error_kfree so the sensor
    struct is freed on devm_cci_regmap_init_i2c() or mt9m113_parse_dt()
    failure. Without it those returns leaked the kzalloc.

  - [High] mt9m113_remove(): reorder so PM disable +
    mt9m113_power_off() + pm_runtime_set_suspended() all run BEFORE
    v4l2_async_unregister_subdev(). When no userspace fd is open on
    /dev/v4l-subdev*, async_unregister synchronously fires the per-
    subdev .release callbacks, the last of which runs kfree(sensor);
    every subsequent use of @sensor or @dev (v4l2_fwnode_endpoint_free,
    mt9m113_power_off) was a use-after-free.

    Reproduced on hardware (HP TouchPad / APQ8060): a plain
       echo 3-003c > /sys/bus/i2c/drivers/mt9m113/unbind
    produces a kernel panic with
       Unable to handle kernel NULL pointer dereference at 0x00000058
       PC is at regulator_bulk_disable+0x4c/0x108
       LR is at mt9m113_power_off+0x78/0x7c
    on the v5 kernel. v6 reorders cleanly survives the same unbind
    plus a 60 s concurrent VIDIOC_QUERYCTRL / S_CTRL spammer.

  - [High] v4l2_fwnode_endpoint_free(&sensor->bus_cfg) moved from
    mt9m113_remove() into mt9m113_release_sensor() (the
    release_count == 0 finalizer). bus_cfg.link_frequencies is
    referenced by the IFP control handler's qmenu_int (stored by
    v4l2_ctrl_new_int_menu(V4L2_CID_LINK_FREQ) in probe). The
    control handler outlives remove() under the deferred-release
    model; freeing the endpoint in remove() would leave qmenu_int
    dangling and turn a later VIDIOC_QUERYMENU into a UAF.

  - [Med] mt9m113_ifp_set_fmt(): "if (sensor->streaming) return
    -EBUSY" narrowed to fire only when fmt->which ==
    V4L2_SUBDEV_FORMAT_ACTIVE. V4L2_SUBDEV_FORMAT_TRY queries are
    scratchpad probes by definition and must always be allowed,
    including while the pipeline is live.

  - [Low] dt-bindings: endpoint properties gain
    data-lanes: { maxItems: 1 } so DT entries that try to assign
    more than one lane fail dt_binding_check at validation time
    rather than only at probe time when the driver enforces
    num_data_lanes == 1.

  - [Prep] mt9m113_start_streaming(): add __must_hold(state->lock)
    annotation so sparse -Wcontext can verify the caller of
    s_stream(1) really did acquire the V4L2 subdev-state lock
    via v4l2_subdev_lock_and_get_active_state() before getting
    here. Today sparse cannot fully prove this (the v4l2-subdev
    framework helpers are not yet __acquires/__releases-tagged
    in include/media/v4l2-subdev.h), but the annotation is
    correct on our side and Just Works once core picks them up.

Changes since v4 (already in v5):

  - 5 findings from Sashiko's review of v4: Kconfig select V4L2_FWNODE;
    stream_context_a propagates SEQ_CMD_RUN poll timeout; start_streaming
    retry loop bypasses runtime-PM (bridge device-link blocks normal
    suspend) by toggling power directly; V4L2_CID_TEST_PATTERN rolls
    back test_pattern_active on apply failure; per-subdev .release
    callbacks defer ctrl handler / entity / sensor-struct teardown past
    v4l2_async_unregister_subdev so in-flight VIDIOC_S_CTRL ioctls do
    not unlock freed memory.

  - 1 regression caught during HW unbind/rebind race testing of the
    above: trailing mt9m113_power_off() in the retry loop double-called
    power_off (each break path already powered the chip down), which
    underflowed clk + regulator refcounts. Dropped the redundant
    trailing call; every break path is now responsible for its own
    final-state.

Changes since v3 (already in v4):

  - Context B (1280x1024 capture) stream-start regression fix: the
    MT9M113_SEQ_STATE_PREVIEW constant was 0x04 (datasheet's "Leave
    preview" transient state); the actual stable preview state is 0x03.

  - Forensic NOTE comment above MODE_TEST_MODE documenting the IFP
    test-pattern generator silicon-removal investigation.
    V4L2_CID_TEST_PATTERN plumbing is retained on the chance a different
    silicon variant or vendor SROM patch enables the TPG block.

Changes since v2 (already in v3):

  - Krzysztof + Sakari binding feedback: maxItems on reset-gpios /
    powerdown-gpios; drop |- from descriptions; drop redundant
    link-frequencies / remote-endpoint / data-lanes; rename i2c0 -> i2c.

  - MAINTAINERS entry per patchwork checkpatch.

Pre-send verification (v6, HP TouchPad APQ8060, kernel
7.1.0-rc1-luneos-geba82cc2ec7a):

  - pre-send-check.sh 8/8 PASS, including:
      * sparse on drivers/media/i2c/mt9m113.c -- clean
      * coccinelle scans for the three regression patterns we just
        fixed (pm_runtime_get_if_in_use() == 0; v4l2_fwnode_endpoint_free
        in remove(); sensor deref after v4l2_async_unregister_subdev)
        -- no matches.

  - Sashiko AI preflight (claude-haiku-4-5): 0 actionable findings;
    16 of 16 prior-stage concerns self-dismissed.

  - HP TouchPad on-HW sweep:
      * test-camera.sh PIX 640x480 + 1280x1024 capture -- PASS
      * 10x rapid pix640 stream-start stress -- 10/10 ok
      * 10x rapid pix1280 stream-start stress (Context B path) -- 10/10 ok
      * 60s mt9m113 sysfs unbind/rebind cycle racing concurrent
        VIDIOC_QUERYCTRL / S_CTRL spammers on /dev/v4l-subdev10 +
        /dev/v4l-subdev11 -- driver survives, re-binds cleanly,
        zero Oops / BUG / null-pointer-deref. This is the exact
        scenario that produced the regulator_bulk_disable NULL deref
        on the v5 kernel; v6 is silent.
      * 120s concurrent multi-subsystem load (camera stream loop +
        dd to eMMC + /dev/urandom + filesystem walk) -- clean.
      * dmesg post-sweep totals: 0 WARN, 0 BUG, 0 Oops, 0 clk-refcount
        underflow, 0 regulator-refcount underflow.

  - Kernel build clean (ARCH=arm cross-compile, CONFIG_VIDEO_MT9M113=y).
  - dt_binding_check clean (0 warnings) after the data-lanes maxItems
    constraint was added.

- Link to v4: https://lore.kernel.org/r/20260606-submit-media-mt9m113-v4-0-046b4cbc7f94@herrie.org
- Link to v5: https://lore.kernel.org/r/20260606-submit-media-mt9m113-v5-0-2088c7358e11@herrie.org

---
Herman van Hazendonk (2):
      dt-bindings: media: i2c: add aptina,mt9m113
      media: i2c: add Aptina MT9M113 1.3 Mpx SoC sensor driver

 .../bindings/media/i2c/aptina,mt9m113.yaml         |  130 +
 MAINTAINERS                                        |    8 +
 drivers/media/i2c/Kconfig                          |   13 +
 drivers/media/i2c/Makefile                         |    1 +
 drivers/media/i2c/mt9m113.c                        | 3306 ++++++++++++++++++++
 5 files changed, 3458 insertions(+)
---
base-commit: 944125b4c454b58d2fe6e35f1087a932b2050dff
change-id: 20260606-submit-media-mt9m113-242c8be69e90

Best regards,
-- 
Herman van Hazendonk <github.com@herrie.org>



^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH v6 1/2] dt-bindings: media: i2c: add aptina,mt9m113
  2026-06-06 19:01 [PATCH v6 0/2] media: i2c: add Aptina MT9M113 image sensor driver Herman van Hazendonk via B4 Relay
@ 2026-06-06 19:01 ` Herman van Hazendonk via B4 Relay
  2026-06-06 19:01 ` [PATCH v6 2/2] media: i2c: add Aptina MT9M113 1.3 Mpx SoC sensor driver Herman van Hazendonk via B4 Relay
  1 sibling, 0 replies; 4+ messages in thread
From: Herman van Hazendonk via B4 Relay @ 2026-06-06 19:01 UTC (permalink / raw)
  To: Mauro Carvalho Chehab, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Sakari Ailus
  Cc: linux-media, devicetree, linux-kernel, Herman van Hazendonk

From: Herman van Hazendonk <github.com@herrie.org>

Add the binding for the Aptina (now ON Semiconductor) MT9M113 1.3
megapixel SoC image sensor with on-die ISP. The chip is used as the
front (user-facing) camera on the HP TouchPad tablet and connects
to the host SoC over MIPI CSI-2.

The binding describes the chip's i2c address, optional reset and
standby GPIOs, the per-supply regulators (VDD, VDD_IO, VDDA), the
external clock input, and the CSI-2 endpoint pads exposed via the
"port" subnode.

Assisted-by: Claude:claude-opus-4-7 Sashiko:claude-haiku-4-5 Sparse:0.6.4 Coccinelle:1.1.1
Signed-off-by: Herman van Hazendonk <github.com@herrie.org>
---
 .../bindings/media/i2c/aptina,mt9m113.yaml         | 122 +++++++++++++++++++++
 1 file changed, 122 insertions(+)

diff --git a/Documentation/devicetree/bindings/media/i2c/aptina,mt9m113.yaml b/Documentation/devicetree/bindings/media/i2c/aptina,mt9m113.yaml
new file mode 100644
index 000000000000..72b827c47b1d
--- /dev/null
+++ b/Documentation/devicetree/bindings/media/i2c/aptina,mt9m113.yaml
@@ -0,0 +1,122 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/media/i2c/aptina,mt9m113.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Aptina MT9M113 1.3 Megapixel CMOS Digital Image Sensor
+
+maintainers:
+  - Herman van Hazendonk <github.com@herrie.org>
+
+description:
+  The Aptina MT9M113 is a 1.3 Megapixel CMOS digital image sensor with
+  1280x1024 active pixels. It supports dual context operation
+  Context A (640x480 preview mode with binning) and Context B (1280x1024
+  capture mode). It is programmable through an I2C interface and outputs
+  image data over a 1-lane MIPI CSI-2 connection.
+
+properties:
+  compatible:
+    const: aptina,mt9m113
+
+  reg:
+    description: I2C device address
+    enum:
+      - 0x3c
+      - 0x48
+      - 0x5d
+
+  clocks:
+    description: EXTCLK clock signal (24 MHz typical)
+    maxItems: 1
+
+  vdd-supply:
+    description:
+      Core digital voltage supply, 1.8V
+
+  vddio-supply:
+    description:
+      I/O digital voltage supply, 1.8V or 2.8V
+
+  vaa-supply:
+    description:
+      Analog voltage supply, 2.8V
+
+  reset-gpios:
+    maxItems: 1
+    description:
+      GPIO connected to the RESET_BAR pin, if any (active low). The MT9M113
+      has a single RESET_BAR pin.
+
+  powerdown-gpios:
+    maxItems: 1
+    description:
+      GPIO connected to the STANDBY/PWDN pin, if any (active high). When
+      asserted, the sensor enters low-power standby mode. The MT9M113 has
+      a single STANDBY/PWDN pin.
+
+  port:
+    $ref: /schemas/graph.yaml#/$defs/port-base
+    additionalProperties: false
+
+    properties:
+      endpoint:
+        $ref: /schemas/media/video-interfaces.yaml#
+        unevaluatedProperties: false
+
+        properties:
+          bus-type:
+            const: 4
+            description: MIPI CSI-2 D-PHY
+
+        required:
+          - bus-type
+          - link-frequencies
+
+    required:
+      - endpoint
+
+required:
+  - compatible
+  - reg
+  - clocks
+  - vdd-supply
+  - vddio-supply
+  - vaa-supply
+  - port
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/gpio/gpio.h>
+    #include <dt-bindings/media/video-interfaces.h>
+
+    i2c {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        camera@3c {
+            compatible = "aptina,mt9m113";
+            reg = <0x3c>;
+
+            clocks = <&mmcc 48>;
+
+            reset-gpios = <&pm8058_gpio 29 GPIO_ACTIVE_LOW>;
+            powerdown-gpios = <&pm8058_gpio 30 GPIO_ACTIVE_HIGH>;
+
+            vddio-supply = <&pm8058_l15>;
+            vdd-supply = <&pm8058_l15>;
+            vaa-supply = <&pm8058_l14>;
+
+            port {
+                mt9m113_ep: endpoint {
+                    bus-type = <MEDIA_BUS_TYPE_CSI2_DPHY>;
+                    link-frequencies = /bits/ 64 <384000000>;
+                    remote-endpoint = <&csiphy1_ep>;
+                };
+            };
+        };
+    };
+...

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 4+ messages in thread

* [PATCH v6 2/2] media: i2c: add Aptina MT9M113 1.3 Mpx SoC sensor driver
  2026-06-06 19:01 [PATCH v6 0/2] media: i2c: add Aptina MT9M113 image sensor driver Herman van Hazendonk via B4 Relay
  2026-06-06 19:01 ` [PATCH v6 1/2] dt-bindings: media: i2c: add aptina,mt9m113 Herman van Hazendonk via B4 Relay
@ 2026-06-06 19:01 ` Herman van Hazendonk via B4 Relay
  2026-06-06 19:13   ` sashiko-bot
  1 sibling, 1 reply; 4+ messages in thread
From: Herman van Hazendonk via B4 Relay @ 2026-06-06 19:01 UTC (permalink / raw)
  To: Mauro Carvalho Chehab, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Sakari Ailus
  Cc: linux-media, devicetree, linux-kernel, Herman van Hazendonk

From: Herman van Hazendonk <github.com@herrie.org>

Add a V4L2 subdev driver for the Aptina MT9M113 1.3 Megapixel SoC
image sensor (1280x1024 active pixel array) with on-chip ISP. The
sensor exposes:

  - dual context operation: Context A (640x480 preview, binned) and
    Context B (1280x1024 capture) selectable at runtime;
  - single-lane MIPI CSI-2 output, YUV422 in UYVY or YUYV byte order
    (the byte order is selected via the chroma/luma swap bit in
    MODE_OUTPUT_FORMAT_{A,B});
  - I2C MCU-indirect register access through the 0x098C/0x0990 page
    indirection pair, in addition to the directly addressable
    register space;
  - V4L2 controls: H/V flip, color effects (none / mono / sepia /
    negative / solarization), power-line frequency, saturation,
    manual or auto exposure, analog gain, auto white balance, and
    a four-entry test pattern selector;
  - a 24 MHz EXTCLK and 1.8/2.8 V supplies.

The MT9M113 MCU intermittently wedges on stream start (SEQ_CMD
stuck, SEQ_STATE never reaching preview/capture; only a full power
cycle recovers it). The driver works around this in two layers:
runtime-PM resume always performs a full power-cycle plus init-table
replay - matching the legacy vendor kernel's per-open behaviour -
so a wedged MCU is recovered each session; and the s_stream(1) path
retries a bounded number of times with a runtime-PM power cycle
between attempts as fallback insurance for the residual failure
rate.

Found on the HP TouchPad (Tenderloin) as the front-facing camera
and on a number of other 1280x1024 mobile/embedded boards.

Assisted-by: Claude:claude-opus-4-7 Sashiko:claude-haiku-4-5 Sparse:0.6.4 Coccinelle:1.1.1
Signed-off-by: Herman van Hazendonk <github.com@herrie.org>
---
 .../bindings/media/i2c/aptina,mt9m113.yaml         |    8 +
 MAINTAINERS                                        |    8 +
 drivers/media/i2c/Kconfig                          |   13 +
 drivers/media/i2c/Makefile                         |    1 +
 drivers/media/i2c/mt9m113.c                        | 3306 ++++++++++++++++++++
 5 files changed, 3336 insertions(+)

diff --git a/Documentation/devicetree/bindings/media/i2c/aptina,mt9m113.yaml b/Documentation/devicetree/bindings/media/i2c/aptina,mt9m113.yaml
index 72b827c47b1d..cfd6711e8250 100644
--- a/Documentation/devicetree/bindings/media/i2c/aptina,mt9m113.yaml
+++ b/Documentation/devicetree/bindings/media/i2c/aptina,mt9m113.yaml
@@ -70,6 +70,14 @@ properties:
             const: 4
             description: MIPI CSI-2 D-PHY
 
+          data-lanes:
+            description:
+              The MT9M113 has a single CSI-2 data lane. Reject DT entries
+              that try to assign more than one lane so a misconfiguration
+              is caught at schema-validation time rather than at runtime
+              when the driver enforces num_data_lanes == 1.
+            maxItems: 1
+
         required:
           - bus-type
           - link-frequencies
diff --git a/MAINTAINERS b/MAINTAINERS
index 2fb1c75afd16..c1ca196180b0 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -18165,6 +18165,14 @@ L:	linux-mtd@lists.infradead.org
 S:	Maintained
 F:	drivers/mtd/devices/docg3*
 
+MT9M113 APTINA SENSOR DRIVER
+M:	Herman van Hazendonk <github.com@herrie.org>
+L:	linux-media@vger.kernel.org
+S:	Maintained
+T:	git git://linuxtv.org/media.git
+F:	Documentation/devicetree/bindings/media/i2c/aptina,mt9m113.yaml
+F:	drivers/media/i2c/mt9m113.c
+
 MT9M114 ONSEMI SENSOR DRIVER
 M:	Laurent Pinchart <laurent.pinchart@ideasonboard.com>
 L:	linux-media@vger.kernel.org
diff --git a/drivers/media/i2c/Kconfig b/drivers/media/i2c/Kconfig
index 8f2ba4121586..31c4f96ae5ee 100644
--- a/drivers/media/i2c/Kconfig
+++ b/drivers/media/i2c/Kconfig
@@ -302,6 +302,19 @@ config VIDEO_MT9M111
 	  This driver supports MT9M111, MT9M112 and MT9M131 cameras from
 	  Micron/Aptina
 
+config VIDEO_MT9M113
+	tristate "Aptina MT9M113 sensor support"
+	select V4L2_CCI_I2C
+	select V4L2_FWNODE
+	help
+	  This is a Video4Linux2 sensor driver for the Aptina MT9M113
+	  1.3 Megapixel CMOS camera sensor. The MT9M113 has dual context
+	  support: Context A (640x480 preview) and Context B (1280x1024
+	  capture).
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called mt9m113.
+
 config VIDEO_MT9M114
 	tristate "onsemi MT9M114 sensor support"
 	select V4L2_CCI_I2C
diff --git a/drivers/media/i2c/Makefile b/drivers/media/i2c/Makefile
index 90b276a7417a..f2ba184dd0e4 100644
--- a/drivers/media/i2c/Makefile
+++ b/drivers/media/i2c/Makefile
@@ -76,6 +76,7 @@ obj-$(CONFIG_VIDEO_ML86V7667) += ml86v7667.o
 obj-$(CONFIG_VIDEO_MSP3400) += msp3400.o
 obj-$(CONFIG_VIDEO_MT9M001) += mt9m001.o
 obj-$(CONFIG_VIDEO_MT9M111) += mt9m111.o
+obj-$(CONFIG_VIDEO_MT9M113) += mt9m113.o
 obj-$(CONFIG_VIDEO_MT9M114) += mt9m114.o
 obj-$(CONFIG_VIDEO_MT9P031) += mt9p031.o
 obj-$(CONFIG_VIDEO_MT9T112) += mt9t112.o
diff --git a/drivers/media/i2c/mt9m113.c b/drivers/media/i2c/mt9m113.c
new file mode 100644
index 000000000000..f9772fab44e2
--- /dev/null
+++ b/drivers/media/i2c/mt9m113.c
@@ -0,0 +1,3306 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * mt9m113.c Aptina MT9M113 sensor driver
+ *
+ * Copyright (C) 2024 Herman van Hazendonk <github.com@herrie.org>
+ *
+ * MT9M113 is a 1.3MP SOC sensor with dual context support:
+ *   Context A: 640x480 preview mode (binned)
+ *   Context B: 1280x1024 capture mode (full resolution)
+ *
+ * Ported from legacy vendor kernel mt9m113.c/mt9m113_reg.c.
+ * Uses MCU indirect access (0x098C/0x0990) for configuration.
+ */
+
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/errno.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/math64.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pm_runtime.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/types.h>
+#include <linux/videodev2.h>
+
+#include <media/v4l2-async.h>
+#include <media/v4l2-cci.h>
+#include <media/v4l2-ctrls.h>
+#include <media/v4l2-device.h>
+#include <media/v4l2-fwnode.h>
+#include <media/v4l2-mediabus.h>
+#include <media/v4l2-subdev.h>
+
+/* Delay before enabling MIPI output, allowing CSIPHY to stabilize. */
+#define MT9M113_PRE_MIPI_DELAY_MS	10
+
+/*
+ * The MT9M113 MCU intermittently wedges on stream start (SEQ_CMD stuck,
+ * 0xa103 timeout, SEQ_STATE never reaching preview 0x04 / capture 0x07).
+ * The failures cluster at the start of a run and then clear, so a bounded
+ * retry that power-cycles the sensor between attempts reliably gets the
+ * stream up.  3 attempts is enough in practice (with the per-session
+ * runtime-PM re-init as the primary fix); a larger cap would stack
+ * power-cycle + ~600-entry init-table replay into a STREAMON latency
+ * userspace clients treat as a hang.  Total attempts (1 initial + retries).
+ */
+#define MT9M113_STREAM_START_RETRIES	3
+
+/* MT9M113 chip ID */
+#define MT9M113_CHIP_ID				CCI_REG16(0x0000)
+#define MT9M113_CHIP_ID_VALUE			0x2480
+
+/* Sysctl registers */
+#define MT9M113_PLL_DIVIDERS			CCI_REG16(0x0010)
+#define MT9M113_PLL_P_DIVIDERS			CCI_REG16(0x0012)
+#define MT9M113_PLL_CONTROL			CCI_REG16(0x0014)
+#define MT9M113_CLOCKS_CONTROL			CCI_REG16(0x0016)
+#define MT9M113_STANDBY_CONTROL			CCI_REG16(0x0018)
+#define MT9M113_STANDBY_CONTROL_ACTIVE		0x0028	/* MCU running, not in standby */
+#define MT9M113_RESET_AND_MISC_CONTROL		CCI_REG16(0x001a)
+#define MT9M113_RESET_SOC			BIT(0)
+#define MT9M113_MCU_BOOT_MODE			CCI_REG16(0x001c)
+
+/* MCU indirect access registers */
+#define MT9M113_MCU_ADDRESS			CCI_REG16(0x098c)
+#define MT9M113_MCU_DATA			CCI_REG16(0x0990)
+
+/* MCU variable addresses */
+#define MT9M113_SEQ_CMD				0xa103
+#define MT9M113_SEQ_CMD_RUN			0x0001
+#define MT9M113_SEQ_CMD_CAPTURE			0x0002
+#define MT9M113_SEQ_CMD_REFRESH			0x0005
+#define MT9M113_SEQ_CMD_REFRESH_MODE		0x0006
+#define MT9M113_SEQ_STATE			0xa104
+/*
+ * Per datasheet R0x0004 seq_state values: 0x02 = Enter preview, 0x03 =
+ * Preview (stable), 0x04 = Leave preview (transient, only seen while
+ * exiting preview for a context switch). 0x07 is stable Capture
+ * (Context B). The Context-B preview-first step must wait for the
+ * stable 0x03, not the transient 0x04 - waiting for 0x04 here misses
+ * the window and times out on every Context B stream-start.
+ */
+#define MT9M113_SEQ_STATE_PREVIEW		0x03
+#define MT9M113_SEQ_STATE_CAPTURE		0x07
+#define MT9M113_SEQ_CAP_MODE			0xa115
+#define MT9M113_SEQ_CAP_MODE_PREVIEW		0x0030	/* continuous preview */
+#define MT9M113_SEQ_CAP_MODE_VIDEO		0x0002	/* stay in Context B */
+
+/* Mode Output dimension/format MCU variables (Driver ID 7) */
+#define MT9M113_MODE_OUTPUT_WIDTH_A		0x2703
+#define MT9M113_MODE_OUTPUT_HEIGHT_A		0x2705
+#define MT9M113_MODE_OUTPUT_WIDTH_B		0x2707
+#define MT9M113_MODE_OUTPUT_HEIGHT_B		0x2709
+#define MT9M113_MODE_OUTPUT_FORMAT_A		0x2755
+#define MT9M113_MODE_OUTPUT_FORMAT_B		0x2757
+#define MT9M113_MODE_OUTPUT_FORMAT_YUV		0x0000
+/*
+ * YUV byte ordering (datasheet table 31 "mode_output_format_a/b"):
+ *   default (0x0000)   = Cb Y0 Cr Y1 = UYVY
+ *   SWAP_LUMA_CHROMA   = Y0 Cb Y1 Cr = YUYV
+ */
+#define MT9M113_MODE_OUTPUT_FORMAT_SWAP_LUMA_CHROMA	BIT(1)
+
+/* Special effects MCU variables */
+#define MT9M113_MODE_SPEC_EFFECTS_A		0x2759
+#define MT9M113_MODE_SPEC_EFFECTS_B		0x275b
+#define MT9M113_SPEC_EFFECTS_DEFAULT		0x6440
+#define MT9M113_SPEC_EFFECTS_MASK		0x0007
+#define MT9M113_SPEC_EFFECTS_NONE		0x0000
+#define MT9M113_SPEC_EFFECTS_MONOCHROME		0x0001
+#define MT9M113_SPEC_EFFECTS_SEPIA		0x0002
+#define MT9M113_SPEC_EFFECTS_NEGATIVE		0x0003
+#define MT9M113_SPEC_EFFECTS_SOLARIZE		0x0004
+
+/* Sensor Read Mode MCU variables (for flip/mirror control) */
+#define MT9M113_SENSOR_READ_MODE_A		0x2717
+#define MT9M113_SENSOR_READ_MODE_B		0x272d
+#define MT9M113_SENSOR_READ_MODE_HMIRROR	BIT(0)
+#define MT9M113_SENSOR_READ_MODE_VMIRROR	BIT(1)
+
+/*
+ * Context A/B sensor configuration MCU variables
+ * These control the sensor readout window and must be re-applied when
+ * switching contexts to ensure correct line counts.
+ */
+#define MT9M113_MODE_SENSOR_ROW_START_A		0x270D
+#define MT9M113_MODE_SENSOR_COL_START_A		0x270F
+#define MT9M113_MODE_SENSOR_ROW_END_A		0x2711
+#define MT9M113_MODE_SENSOR_COL_END_A		0x2713
+#define MT9M113_MODE_SENSOR_ROW_SPEED_A		0x2715
+#define MT9M113_MODE_SENSOR_FRAME_LENGTH_A	0x271F
+#define MT9M113_MODE_SENSOR_LINE_LENGTH_PCK_A	0x2721
+
+#define MT9M113_MODE_SENSOR_ROW_START_B		0x2723
+#define MT9M113_MODE_SENSOR_COL_START_B		0x2725
+#define MT9M113_MODE_SENSOR_ROW_END_B		0x2727
+#define MT9M113_MODE_SENSOR_COL_END_B		0x2729
+#define MT9M113_MODE_SENSOR_ROW_SPEED_B		0x272B
+#define MT9M113_MODE_SENSOR_FRAME_LENGTH_B	0x2735
+#define MT9M113_MODE_SENSOR_LINE_LENGTH_PCK_B	0x2737
+
+/* Context A sensor config values (from legacy vendor driver, 640x480 binned) */
+#define MT9M113_CONTEXT_A_ROW_START		0x0000
+#define MT9M113_CONTEXT_A_COL_START		0x0000
+#define MT9M113_CONTEXT_A_ROW_END		0x03CD	/* 973 */
+#define MT9M113_CONTEXT_A_COL_END		0x050D	/* 1293 */
+#define MT9M113_CONTEXT_A_ROW_SPEED		0x2111
+#define MT9M113_CONTEXT_A_READ_MODE		0x046C	/* Binning enabled */
+#define MT9M113_CONTEXT_A_FRAME_LENGTH		0x032E	/* 814 lines (incl vblank) */
+#define MT9M113_CONTEXT_A_LINE_LENGTH_PCK	0x04CC	/* 1228 pixclks/line */
+
+/* Context B sensor config values (from legacy vendor driver, 1280x1024 full res) */
+#define MT9M113_CONTEXT_B_ROW_START		0x0004
+#define MT9M113_CONTEXT_B_COL_START		0x0004
+#define MT9M113_CONTEXT_B_ROW_END		0x040B	/* 1035 */
+#define MT9M113_CONTEXT_B_COL_END		0x050B	/* 1291 */
+#define MT9M113_CONTEXT_B_ROW_SPEED		0x2111
+#define MT9M113_CONTEXT_B_READ_MODE		0x0024	/* Full resolution */
+#define MT9M113_CONTEXT_B_FRAME_LENGTH		0x0559	/* 1369 lines (incl vblank) */
+#define MT9M113_CONTEXT_B_LINE_LENGTH_PCK	0x0722	/* 1826 pixclks/line */
+
+/* Auto Exposure MCU variables (for preview vs snapshot optimization) */
+#define MT9M113_AE_MAX_INDEX			0xa20c
+#define MT9M113_AE_MAX_VIRTGAIN			0xa20e
+#define MT9M113_AE_MAX_DGAIN_AE1		0xa21a
+#define MT9M113_AE_JUMP_DIVISOR			0xa21c
+#define MT9M113_AE_SKIP_FRAMES			0xa21e
+
+/* Flicker detection MCU variable */
+#define MT9M113_FD_MODE				0xa404
+#define MT9M113_FD_MODE_DISABLED		0x00
+#define MT9M113_FD_MODE_50HZ			0x01
+#define MT9M113_FD_MODE_60HZ			0x02
+#define MT9M113_FD_MODE_AUTO			0x03
+
+/* AWB/Color MCU variables */
+#define MT9M113_AWB_SATURATION			0xa354
+#define MT9M113_AWB_MODE			0xa34a
+
+/* Test pattern MCU variables (mode_common_mode_settings) */
+#define MT9M113_CAM_MODE_SELECT			0xc84c
+#define MT9M113_CAM_MODE_SELECT_NORMAL		0x00
+
+/*
+ * Test pattern register (Driver 7 R0x0066 mode_common_mode_settings_test_mode,
+ * MT9M113 datasheet "Table 31: Driver ID = 7: Mode Variables"). MCU logical
+ * addresses are NOT a clean (driver_id << 12) | offset encoding - each
+ * driver has its own hardcoded base page set by the chip firmware. Driver 7
+ * Mode variables share base 0x2700 (see MT9M113_MODE_OUTPUT_WIDTH_A = 0x2703
+ * for R0x0003, MT9M113_MODE_SPEC_EFFECTS_A = 0x2759 for R0x0059, etc.), so
+ * R0x0066 maps to 0x2766.
+ *
+ * Per the datasheet "Test Pattern Generator. Changes take effect only
+ * after REFRESH command", so writes are paired with mt9m113_refresh()
+ * when streaming. The datasheet also recommends disabling the MCU before
+ * enabling test patterns (BOOT_MODE=1 halt), which
+ * mt9m113_apply_test_pattern() performs after the REFRESH succeeds.
+ *
+ * Do NOT go via the CAM_MODE driver's test-pattern knob (CAM_MODE_SELECT =
+ * 0x02 TEST_PATTERN + CAM_MODE_TEST_PATTERN_SELECT in Driver 8): switching
+ * CAM_MODE into TEST_PATTERN mode wedges the sequencer so SEQ_CMD never
+ * processes REFRESH and stream_on() hangs.
+ *
+ * NOTE: forensic investigation on observed silicon shows the IFP TPG MUX
+ * does NOT actually engage despite mode_test_mode accepting every value:
+ *
+ *   - R0x2766 (mode_test_mode) is a read/write logical variable, but the
+ *     MCU firmware resets it to a default (0x8e8e) on every standby wake.
+ *   - Hardware registers associated with the TPG MUX (R0x3246-R0x3252)
+ *     are non-responsive dead zones, and bit 4 of R0x321C is hardwired
+ *     to 0 - both classic Aptina foundry-removal fingerprints.
+ *   - No raw sensor-core TPG (R0x3070-R0x3071) is present.
+ *   - Confirmed by stripping the color pipeline (R0x3210=0) and
+ *     verifying that even in a "naked" IFP state the live sensor data
+ *     persists without any pattern injection.
+ *
+ * The V4L2_CID_TEST_PATTERN plumbing below is intentionally retained on
+ * the (faint) chance a different silicon variant or vendor SROM patch
+ * actually wires up the TPG block. Setting test_pattern > 0 on the
+ * current die is harmless - the writes succeed but no pattern appears.
+ */
+#define MT9M113_MODE_TEST_MODE			0x2766
+#define MT9M113_TEST_MODE_DISABLED		0x00
+#define MT9M113_TEST_MODE_SOLID_WHITE		0x01
+#define MT9M113_TEST_MODE_GREY_RAMP		0x02
+#define MT9M113_TEST_MODE_COLOR_BAR_RAMP	0x03
+#define MT9M113_TEST_MODE_SOLID_WHITE_COLOR_BARS 0x04
+#define MT9M113_TEST_MODE_NOISE			0x05
+
+/* Double buffer control register */
+#define MT9M113_DOUBLE_BUFFER_CONTROL		CCI_REG16(0x0248)
+#define MT9M113_DOUBLE_BUFFER_SUSPEND		BIT(15)
+
+/* Auto Exposure MCU variables */
+#define MT9M113_AE_GATE				0xa207
+#define MT9M113_AE_GATE_ENABLE			0x0000
+#define MT9M113_AE_GATE_DISABLE			0x00FF
+
+/* Sensor core exposure/gain registers */
+#define MT9M113_COARSE_IT_TIME_A		CCI_REG16(0x3012)
+#define MT9M113_COARSE_IT_TIME_B		CCI_REG16(0x3014)
+#define MT9M113_ANALOG_GAIN			CCI_REG16(0x3028)
+
+/* Sensor core registers */
+#define MT9M113_RESET_REGISTER			CCI_REG16(0x301a)
+#define MT9M113_RESET_REG_STREAMING		0x120C
+#define MT9M113_OFIFO_CONTROL_STATUS		CCI_REG16(0x321c)
+#define MT9M113_OFIFO_BYPASS			0x0003	/* FIFO bypass (YUV path) */
+
+/* Color pipeline (ISP) control */
+#define MT9M113_COLOR_PIPELINE_CONTROL		CCI_REG16(0x3210)
+#define MT9M113_COLOR_PIPELINE_DEFAULT		0x01b8	/* scaler+gamma+ap+shading */
+
+/* OUTPUT_CONTROL register (0x3400) - MIPI control */
+#define MT9M113_OUTPUT_CONTROL			CCI_REG16(0x3400)
+#define MT9M113_OUTPUT_CONTROL_MIPI_ENABLE	0x7A08	/* YUV422 dt=0x1E */
+
+/* CUSTOM_SHORT_PKT register */
+#define MT9M113_CUSTOM_SHORT_PKT		CCI_REG16(0x3404)
+#define MT9M113_CUSTOM_SHORT_PKT_FRAME_CNT_EN	0x0080
+
+/* Pixel array dimensions */
+#define MT9M113_PIXEL_ARRAY_WIDTH		1296U
+#define MT9M113_PIXEL_ARRAY_HEIGHT		1040U
+
+/* -----------------------------------------------------------------------------
+ * Data Structures
+ */
+
+struct mt9m113_format_info {
+	u32 code;
+};
+
+struct mt9m113 {
+	struct i2c_client *client;
+	struct regmap *regmap;
+
+	struct clk *clk;
+	struct gpio_desc *reset;
+	struct gpio_desc *powerdown;
+	struct regulator_bulk_data supplies[3];
+	struct v4l2_fwnode_endpoint bus_cfg;
+
+	unsigned int pixrate;
+	s64 link_freq;
+	bool streaming;
+	bool test_pattern_active;
+	u8 test_pattern_value;	/* last V4L2_CID_TEST_PATTERN menu index */
+
+	/*
+	 * Set true after a manual mt9m113_power_off() inside the
+	 * start_streaming retry loop so the next runtime_suspend skips
+	 * its own power_off (which would underflow clk / regulator
+	 * refcounts). Cleared by runtime_suspend (consumed) or by a
+	 * successful runtime_resume.
+	 */
+	bool chip_off;
+
+	/*
+	 * Decremented by each subdev's release callback; sensor struct is
+	 * freed once both /dev/v4l-subdev* nodes have no open fds left.
+	 */
+	atomic_t release_count;
+
+	/* Pixel Array sub-device */
+	struct {
+		struct v4l2_subdev sd;
+		struct media_pad pad;
+		struct v4l2_ctrl_handler hdl;
+	} pa;
+
+	/* Image Flow Processor sub-device */
+	struct {
+		struct v4l2_subdev sd;
+		struct media_pad pads[2];
+
+		struct v4l2_ctrl_handler hdl;
+	} ifp;
+};
+
+/* -----------------------------------------------------------------------------
+ * Formats
+ */
+
+/*
+ * Source-pad output formats.  Only YUV422 is exposed: MT9M113_OUTPUT_CONTROL
+ * advertises the YUV422 CSI-2 data type (0x1E) unconditionally on the wire,
+ * and adding RGB565 would require also driving the matching CSI-2 dt (0x22)
+ * out of OUTPUT_CONTROL — verifying the exact bit layout for that needs the
+ * sensor datasheet and a hardware re-test, so RGB is deferred to a follow-up.
+ */
+static const struct mt9m113_format_info mt9m113_format_infos[] = {
+	{ .code = MEDIA_BUS_FMT_UYVY8_1X16 },
+	{ .code = MEDIA_BUS_FMT_YUYV8_1X16 },
+};
+
+static const struct mt9m113_format_info *
+mt9m113_format_info(u32 code)
+{
+	unsigned int i;
+
+	for (i = 0; i < ARRAY_SIZE(mt9m113_format_infos); i++) {
+		if (mt9m113_format_infos[i].code == code)
+			return &mt9m113_format_infos[i];
+	}
+
+	return &mt9m113_format_infos[0];
+}
+
+/*
+ * Map a V4L2 mbus code to the value programmed into MODE_OUTPUT_FORMAT_{A,B}.
+ * Driving the chroma/luma swap bit is what differentiates UYVY from YUYV;
+ * without it both codes would emit the same bytes on the MIPI bus.
+ */
+static u16 mt9m113_format_val(u32 code)
+{
+	switch (code) {
+	case MEDIA_BUS_FMT_YUYV8_1X16:
+		return MT9M113_MODE_OUTPUT_FORMAT_YUV |
+		       MT9M113_MODE_OUTPUT_FORMAT_SWAP_LUMA_CHROMA;
+	case MEDIA_BUS_FMT_UYVY8_1X16:
+	default:
+		return MT9M113_MODE_OUTPUT_FORMAT_YUV;
+	}
+}
+
+/* -----------------------------------------------------------------------------
+ * MCU Variable Access Helpers
+ */
+
+static int mt9m113_write_mcu_var(struct mt9m113 *sensor, u16 addr, u16 value)
+{
+	int ret = 0;
+
+	cci_write(sensor->regmap, MT9M113_MCU_ADDRESS, addr, &ret);
+	cci_write(sensor->regmap, MT9M113_MCU_DATA, value, &ret);
+	return ret;
+}
+
+static int mt9m113_read_mcu_var(struct mt9m113 *sensor, u16 addr, u64 *value)
+{
+	int ret;
+
+	ret = cci_write(sensor->regmap, MT9M113_MCU_ADDRESS, addr, NULL);
+	if (ret)
+		return ret;
+	return cci_read(sensor->regmap, MT9M113_MCU_DATA, value, NULL);
+}
+
+static int mt9m113_poll_mcu_var(struct mt9m113 *sensor, u16 addr,
+				u16 expected, unsigned int timeout_ms)
+{
+	unsigned int i;
+	u64 value = 0;
+	int ret;
+
+	for (i = 0; i < timeout_ms / 10; i++) {
+		ret = mt9m113_read_mcu_var(sensor, addr, &value);
+		if (ret < 0)
+			return ret;
+		if (value == expected)
+			return 0;
+		usleep_range(10000, 11000);
+	}
+
+	dev_err(&sensor->client->dev,
+		"MCU var 0x%04x timeout (got 0x%llx, expected 0x%04x)\n",
+		addr, value, expected);
+	return -ETIMEDOUT;
+}
+
+/*
+ * Ensure MCU sequencer is idle before issuing a new command.
+ * This prevents cascading failures where a timed-out command
+ * causes subsequent commands to also fail.
+ */
+static int mt9m113_seq_cmd_ready(struct mt9m113 *sensor)
+{
+	u64 seq_cmd;
+	int ret;
+
+	ret = mt9m113_read_mcu_var(sensor, MT9M113_SEQ_CMD, &seq_cmd);
+	if (ret < 0)
+		return ret;
+
+	if (seq_cmd == 0)
+		return 0;  /* Already idle */
+
+	/*
+	 * A previous command is still pending. This can happen if:
+	 * 1. Previous streaming start failed mid-way
+	 * 2. The MCU is slow to process a command
+	 *
+	 * Wait up to 2 seconds for it to complete. This is longer than
+	 * normal command timeouts to handle recovery scenarios.
+	 */
+	dev_dbg(&sensor->client->dev,
+		 "MT9M113: SEQ_CMD=0x%llx, waiting for idle\n", seq_cmd);
+
+	return mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 2000);
+}
+
+/*
+ * mt9m113_refresh - Issue REFRESH_MODE + REFRESH sequence
+ *
+ * Per datasheet: "It is recommended that refresh and refresh mode commands
+ * always be run together, and that refresh mode should be issued BEFORE
+ * the refresh command."
+ */
+static int mt9m113_refresh(struct mt9m113 *sensor)
+{
+	int ret;
+
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CMD,
+				    MT9M113_SEQ_CMD_REFRESH_MODE);
+	if (ret)
+		return ret;
+
+	ret = mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 500);
+	if (ret < 0)
+		dev_warn(&sensor->client->dev, "MT9M113: REFRESH_MODE timeout\n");
+
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CMD,
+				    MT9M113_SEQ_CMD_REFRESH);
+	if (ret)
+		return ret;
+
+	ret = mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 500);
+	if (ret < 0)
+		dev_warn(&sensor->client->dev, "MT9M113: REFRESH timeout\n");
+
+	return 0;
+}
+
+/*
+ * Re-program SENSOR_READ_MODE_{A,B} for the context geometry while keeping
+ * the live HFLIP/VFLIP bits, which the V4L2 control handler has already
+ * applied via s_ctrl().  Writing CONTEXT_X_READ_MODE blindly would clear
+ * mirror bits 0/1 and silently undo the user's flip on first stream-start.
+ */
+static int mt9m113_apply_read_mode(struct mt9m113 *sensor, u16 reg, u16 base)
+{
+	const u16 mirror_mask = MT9M113_SENSOR_READ_MODE_HMIRROR |
+				MT9M113_SENSOR_READ_MODE_VMIRROR;
+	u64 cur = 0;
+	int ret;
+
+	ret = mt9m113_read_mcu_var(sensor, reg, &cur);
+	if (ret)
+		return ret;
+
+	return mt9m113_write_mcu_var(sensor, reg,
+				     (base & ~mirror_mask) |
+				     ((u16)cur & mirror_mask));
+}
+
+/**
+ * mt9m113_configure_sensor_context - Configure sensor readout parameters
+ * @sensor: MT9M113 sensor device
+ * @use_context_b: true for Context B (1280x1024), false for Context A (640x480)
+ *
+ * Re-applies the sensor configuration registers for the selected context.
+ * This is necessary because the MT9M113's MCU variables can become corrupted
+ * or stale after switching between contexts. Without explicitly re-writing
+ * these values, Context A may use incorrect row_end and read_mode values
+ * after running Context B, resulting in wrong line counts and corrupted images.
+ *
+ * Must be called before mt9m113_refresh() to ensure the new values take effect.
+ */
+static int mt9m113_configure_sensor_context(struct mt9m113 *sensor,
+					    bool use_context_b)
+{
+	struct device *dev = &sensor->client->dev;
+	int ret;
+
+	if (use_context_b) {
+		/* Context B: 1280x1024 full resolution */
+		dev_dbg(dev, "MT9M113: Configuring Context B sensor params\n");
+
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_ROW_START_B,
+					    MT9M113_CONTEXT_B_ROW_START);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_COL_START_B,
+					    MT9M113_CONTEXT_B_COL_START);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_ROW_END_B,
+					    MT9M113_CONTEXT_B_ROW_END);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_COL_END_B,
+					    MT9M113_CONTEXT_B_COL_END);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_ROW_SPEED_B,
+					    MT9M113_CONTEXT_B_ROW_SPEED);
+		if (ret)
+			return ret;
+		ret = mt9m113_apply_read_mode(sensor,
+					      MT9M113_SENSOR_READ_MODE_B,
+					      MT9M113_CONTEXT_B_READ_MODE);
+		if (ret)
+			return ret;
+		/*
+		 * Frame/line length define the total frame period (active +
+		 * blanking). They must be re-applied with the rest of the
+		 * context geometry: the VFE CAMIF counts lines to find frame
+		 * boundaries (no MIPI FE short packet), so a stale period makes
+		 * the captured image walk vertically frame-over-frame.
+		 */
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_FRAME_LENGTH_B,
+					    MT9M113_CONTEXT_B_FRAME_LENGTH);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_LINE_LENGTH_PCK_B,
+					    MT9M113_CONTEXT_B_LINE_LENGTH_PCK);
+		if (ret)
+			return ret;
+	} else {
+		/* Context A: 640x480 binned preview */
+		dev_dbg(dev, "MT9M113: Configuring Context A sensor params\n");
+
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_ROW_START_A,
+					    MT9M113_CONTEXT_A_ROW_START);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_COL_START_A,
+					    MT9M113_CONTEXT_A_COL_START);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_ROW_END_A,
+					    MT9M113_CONTEXT_A_ROW_END);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_COL_END_A,
+					    MT9M113_CONTEXT_A_COL_END);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_ROW_SPEED_A,
+					    MT9M113_CONTEXT_A_ROW_SPEED);
+		if (ret)
+			return ret;
+		ret = mt9m113_apply_read_mode(sensor,
+					      MT9M113_SENSOR_READ_MODE_A,
+					      MT9M113_CONTEXT_A_READ_MODE);
+		if (ret)
+			return ret;
+		/* See Context B above: frame period must track the geometry. */
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_FRAME_LENGTH_A,
+					    MT9M113_CONTEXT_A_FRAME_LENGTH);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor,
+					    MT9M113_MODE_SENSOR_LINE_LENGTH_PCK_A,
+					    MT9M113_CONTEXT_A_LINE_LENGTH_PCK);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+/* -----------------------------------------------------------------------------
+ * Double Buffer Control
+ *
+ * The MT9M113 uses double buffering for context parameters. Setting bit 15
+ * of register 0x0248 suspends updates from shadow to active registers,
+ * allowing atomic multi-register configuration changes.
+ */
+
+static int mt9m113_double_buffer_suspend(struct mt9m113 *sensor)
+{
+	u64 value;
+	int ret;
+
+	ret = cci_read(sensor->regmap, MT9M113_DOUBLE_BUFFER_CONTROL, &value, NULL);
+	if (ret)
+		return ret;
+
+	value |= MT9M113_DOUBLE_BUFFER_SUSPEND;
+	return cci_write(sensor->regmap, MT9M113_DOUBLE_BUFFER_CONTROL, value, NULL);
+}
+
+static int mt9m113_double_buffer_resume(struct mt9m113 *sensor)
+{
+	u64 value;
+	int ret;
+
+	ret = cci_read(sensor->regmap, MT9M113_DOUBLE_BUFFER_CONTROL, &value, NULL);
+	if (ret)
+		return ret;
+
+	value &= ~MT9M113_DOUBLE_BUFFER_SUSPEND;
+	return cci_write(sensor->regmap, MT9M113_DOUBLE_BUFFER_CONTROL, value, NULL);
+}
+
+/* -----------------------------------------------------------------------------
+ * Initialization Table (from legacy vendor kernel mt9m113_reg.c)
+ */
+
+struct mt9m113_reg_entry {
+	u16 reg;
+	u16 value;
+	u16 delay_ms;
+};
+
+static const struct mt9m113_reg_entry mt9m113_init_table[] = {
+	/* OFIFO control */
+	{ 0x321C, 0x0003, 0 },
+
+	/* Context A output (640x480 preview) */
+	{ 0x098C, 0x2703, 0 },
+	{ 0x0990, 0x0280, 0 },
+	{ 0x098C, 0x2705, 0 },
+	{ 0x0990, 0x01E0, 0 },
+
+	/* Context B output (1280x1024 capture) */
+	{ 0x098C, 0x2707, 0 },
+	{ 0x0990, 0x0500, 0 },
+	{ 0x098C, 0x2709, 0 },
+	{ 0x0990, 0x0400, 0 },
+
+	/* Context A sensor configuration */
+	{ 0x098C, 0x270D, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0x270F, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0x2711, 0 },		/* MODE_SENSOR_ROW_END_A */
+	{ 0x0990, 0x03CD, 0 },		/* 973 (from legacy vendor driver) */
+	{ 0x098C, 0x2713, 0 },
+	{ 0x0990, 0x050D, 0 },
+	{ 0x098C, 0x2715, 0 },
+	{ 0x0990, 0x2111, 0 },
+	{ 0x098C, 0x2717, 0 },
+	{ 0x0990, 0x046C, 0 },
+	{ 0x098C, 0x2719, 0 },
+	{ 0x0990, 0x00AC, 0 },
+	{ 0x098C, 0x271B, 0 },
+	{ 0x0990, 0x01F1, 0 },
+	{ 0x098C, 0x271D, 0 },
+	{ 0x0990, 0x013F, 0 },
+	{ 0x098C, 0x271F, 0 },
+	{ 0x0990, 0x032E, 0 },
+	{ 0x098C, 0x2721, 0 },
+	{ 0x0990, 0x04CC, 0 },
+
+	/* Context B sensor configuration */
+	{ 0x098C, 0x2723, 0 },
+	{ 0x0990, 0x0004, 0 },
+	{ 0x098C, 0x2725, 0 },
+	{ 0x0990, 0x0004, 0 },
+	{ 0x098C, 0x2727, 0 },
+	{ 0x0990, 0x040B, 0 },
+	{ 0x098C, 0x2729, 0 },
+	{ 0x0990, 0x050B, 0 },
+	{ 0x098C, 0x272B, 0 },
+	{ 0x0990, 0x2111, 0 },
+	{ 0x098C, 0x272D, 0 },
+	{ 0x0990, 0x0024, 0 },
+	{ 0x098C, 0x272F, 0 },
+	{ 0x0990, 0x004C, 0 },
+	{ 0x098C, 0x2731, 0 },
+	{ 0x0990, 0x00F9, 0 },
+	{ 0x098C, 0x2733, 0 },
+	{ 0x0990, 0x00A7, 0 },
+	{ 0x098C, 0x2735, 0 },
+	{ 0x0990, 0x0559, 0 },
+	{ 0x098C, 0x2737, 0 },
+	{ 0x0990, 0x0722, 0 },
+
+	/* Crop configuration - Context A */
+	{ 0x098C, 0x2739, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0x273B, 0 },
+	{ 0x0990, 0x027F, 0 },
+	{ 0x098C, 0x273D, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0x273F, 0 },
+	{ 0x0990, 0x01DF, 0 },
+
+	/* Crop configuration - Context B */
+	{ 0x098C, 0x2747, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0x2749, 0 },
+	{ 0x0990, 0x04FF, 0 },
+	{ 0x098C, 0x274B, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0x274D, 0 },
+	{ 0x0990, 0x03FF, 0 },
+
+	/* Flicker detection */
+	{ 0x098C, 0x222D, 0 },
+	{ 0x0990, 0x00CC, 0 },
+	{ 0x098C, 0xA404, 0 },
+	{ 0x0990, 0x0010, 0 },
+	{ 0x098C, 0xA408, 0 },
+	{ 0x0990, 0x0032, 0 },
+	{ 0x098C, 0xA409, 0 },
+	{ 0x0990, 0x0034, 0 },
+	{ 0x098C, 0xA40A, 0 },
+	{ 0x0990, 0x003C, 0 },
+	{ 0x098C, 0xA40B, 0 },
+	{ 0x0990, 0x003E, 0 },
+	{ 0x098C, 0x2411, 0 },
+	{ 0x0990, 0x00CC, 0 },
+	{ 0x098C, 0x2413, 0 },
+	{ 0x0990, 0x00F4, 0 },
+	{ 0x098C, 0x2415, 0 },
+	{ 0x0990, 0x0089, 0 },
+	{ 0x098C, 0x2417, 0 },
+	{ 0x0990, 0x00A4, 0 },
+	{ 0x098C, 0xA40D, 0 },
+	{ 0x0990, 0x0002, 0 },
+	{ 0x098C, 0xA40E, 0 },
+	{ 0x0990, 0x0003, 0 },
+	{ 0x098C, 0xA410, 0 },
+	{ 0x0990, 0x000A, 0 },
+
+	/* Sensor core reserved registers */
+	{ 0x3044, 0x0504, 0 },
+	{ 0x3086, 0x24F7, 0 },
+	{ 0x3088, 0xF059, 0 },
+	{ 0x3090, 0x0716, 0 },
+	{ 0x3092, 0xAB1F, 0 },
+	{ 0x30D4, 0x9020, 0 },
+	{ 0x30E2, 0x6645, 0 },
+	{ 0x30E4, 0x7A66, 0 },
+	{ 0x30E6, 0x6652, 0 },
+	{ 0x30E8, 0x7766, 0 },
+	{ 0x30EA, 0x2E03, 0 },
+	{ 0x30EC, 0x452E, 0 },
+	{ 0x30EE, 0x2E17, 0 },
+	{ 0x30F0, 0x452E, 0 },
+	{ 0x30F6, 0x0501, 0 },
+	{ 0x30F8, 0x0501, 0 },
+	{ 0x30FA, 0x0401, 0 },
+	{ 0x30FC, 0x0401, 0 },
+	{ 0x30FE, 0x5145, 0 },
+	{ 0x3100, 0x4F45, 0 },
+	{ 0x3102, 0x652E, 0 },
+	{ 0x3104, 0x7552, 0 },
+	{ 0x3106, 0x2D05, 0 },
+	{ 0x3108, 0x4405, 0 },
+	{ 0x311A, 0x5045, 0 },
+	{ 0x311E, 0x0601, 0 },
+	{ 0x3122, 0x0601, 0 },
+	{ 0x316C, 0x8406, 0 },
+
+	/* Noise reduction */
+	{ 0x098C, 0xAB2D, 0 },
+	{ 0x0990, 0x002A, 0 },
+	{ 0x098C, 0xAB31, 0 },
+	{ 0x0990, 0x002E, 0 },
+
+	/* Low-light enhancement */
+	{ 0x098C, 0x2B28, 0 },
+	{ 0x0990, 0x1F40, 0 },
+	{ 0x098C, 0x2B2A, 0 },
+	{ 0x0990, 0x3A98, 0 },
+	{ 0x098C, 0x2B38, 0 },
+	{ 0x0990, 0x1F40, 0 },
+	{ 0x098C, 0x2B3A, 0 },
+	{ 0x0990, 0x3A98, 0 },
+
+	/* AE settings */
+	{ 0x098C, 0x2257, 0 },
+	{ 0x0990, 0x2710, 0 },
+	{ 0x098C, 0x2250, 0 },
+	{ 0x0990, 0x1B58, 0 },
+	{ 0x098C, 0x2252, 0 },
+	{ 0x0990, 0x32C8, 0 },
+	{ 0x098C, 0xA24B, 0 },
+	{ 0x0990, 0x0082, 0 },
+
+	/* Aperture */
+	{ 0x326C, 0x0C00, 0 },
+
+	/* More Context A settings */
+	{ 0x098C, 0x2717, 0 },
+	{ 0x0990, 0x046C, 0 },
+	{ 0x098C, 0x2719, 0 },
+	{ 0x0990, 0x00AC, 0 },
+	{ 0x098C, 0x271B, 0 },
+	{ 0x0990, 0x01F1, 0 },
+	{ 0x098C, 0x271D, 0 },
+	{ 0x0990, 0x013F, 0 },
+	{ 0x098C, 0x271F, 0 },
+	{ 0x0990, 0x032E, 0 },
+	{ 0x098C, 0x2721, 0 },
+	{ 0x0990, 0x04CC, 0 },
+	{ 0x098C, 0x275F, 0 },
+	{ 0x0990, 0x0596, 0 },
+	{ 0x098C, 0x2761, 0 },
+	{ 0x0990, 0x0094, 0 },
+
+	/* Lens shading correction */
+	{ 0x364E, 0x07B0, 0 },
+	{ 0x3650, 0x7E0E, 0 },
+	{ 0x3652, 0x3D31, 0 },
+	{ 0x3654, 0x80AE, 0 },
+	{ 0x3656, 0xE131, 0 },
+	{ 0x3658, 0x01B0, 0 },
+	{ 0x365A, 0x878D, 0 },
+	{ 0x365C, 0x2671, 0 },
+	{ 0x365E, 0x7D2D, 0 },
+	{ 0x3660, 0xA5D1, 0 },
+	{ 0x3662, 0x03B0, 0 },
+	{ 0x3664, 0x5A0E, 0 },
+	{ 0x3666, 0x0E71, 0 },
+	{ 0x3668, 0x99EE, 0 },
+	{ 0x366A, 0xA671, 0 },
+	{ 0x366C, 0x0170, 0 },
+	{ 0x366E, 0xF44D, 0 },
+	{ 0x3670, 0x2971, 0 },
+	{ 0x3672, 0x2D4A, 0 },
+	{ 0x3674, 0xD671, 0 },
+	{ 0x3676, 0x674C, 0 },
+	{ 0x3678, 0x748D, 0 },
+	{ 0x367A, 0x3FEE, 0 },
+	{ 0x367C, 0x89AE, 0 },
+	{ 0x367E, 0xB410, 0 },
+	{ 0x3680, 0x168C, 0 },
+	{ 0x3682, 0xC56D, 0 },
+	{ 0x3684, 0x7CAC, 0 },
+	{ 0x3686, 0x038F, 0 },
+	{ 0x3688, 0xA86F, 0 },
+	{ 0x368A, 0xDB6B, 0 },
+	{ 0x368C, 0xA2AE, 0 },
+	{ 0x368E, 0xFA8D, 0 },
+	{ 0x3690, 0x5C8E, 0 },
+	{ 0x3692, 0x740C, 0 },
+	{ 0x3694, 0x9F4B, 0 },
+	{ 0x3696, 0x1C4D, 0 },
+	{ 0x3698, 0x978D, 0 },
+	{ 0x369A, 0x21EC, 0 },
+	{ 0x369C, 0xF5AD, 0 },
+	{ 0x369E, 0x7D10, 0 },
+	{ 0x36A0, 0x3E2E, 0 },
+	{ 0x36A2, 0x8953, 0 },
+	{ 0x36A4, 0xD910, 0 },
+	{ 0x36A6, 0x3033, 0 },
+	{ 0x36A8, 0x06D1, 0 },
+	{ 0x36AA, 0xAD4E, 0 },
+	{ 0x36AC, 0xD2D2, 0 },
+	{ 0x36AE, 0x5CCE, 0 },
+	{ 0x36B0, 0x3B93, 0 },
+	{ 0x36B2, 0x50D0, 0 },
+	{ 0x36B4, 0x79AD, 0 },
+	{ 0x36B6, 0xDFF2, 0 },
+	{ 0x36B8, 0x88AF, 0 },
+	{ 0x36BA, 0x2453, 0 },
+	{ 0x36BC, 0x0051, 0 },
+	{ 0x36BE, 0x81CF, 0 },
+	{ 0x36C0, 0x8313, 0 },
+	{ 0x36C2, 0x2250, 0 },
+	{ 0x36C4, 0x4A53, 0 },
+	{ 0x36C6, 0x0C8D, 0 },
+	{ 0x36C8, 0x362B, 0 },
+	{ 0x36CA, 0xAD51, 0 },
+	{ 0x36CC, 0xA470, 0 },
+	{ 0x36CE, 0x3DD2, 0 },
+	{ 0x36D0, 0x174C, 0 },
+	{ 0x36D2, 0x152F, 0 },
+	{ 0x36D4, 0x82F1, 0 },
+	{ 0x36D6, 0xDED0, 0 },
+	{ 0x36D8, 0x6F12, 0 },
+	{ 0x36DA, 0xD36C, 0 },
+	{ 0x36DC, 0x51AE, 0 },
+	{ 0x36DE, 0xD0AE, 0 },
+	{ 0x36E0, 0x274E, 0 },
+	{ 0x36E2, 0x25F2, 0 },
+	{ 0x36E4, 0xDCCA, 0 },
+	{ 0x36E6, 0x438E, 0 },
+	{ 0x36E8, 0xD64E, 0 },
+	{ 0x36EA, 0x8A71, 0 },
+	{ 0x36EC, 0x1492, 0 },
+	{ 0x36EE, 0xD5B1, 0 },
+	{ 0x36F0, 0xEBF0, 0 },
+	{ 0x36F2, 0x53F3, 0 },
+	{ 0x36F4, 0x3492, 0 },
+	{ 0x36F6, 0x9AF4, 0 },
+	{ 0x36F8, 0x8BF1, 0 },
+	{ 0x36FA, 0x204F, 0 },
+	{ 0x36FC, 0x3A93, 0 },
+	{ 0x36FE, 0xB551, 0 },
+	{ 0x3700, 0xE214, 0 },
+	{ 0x3702, 0xF2B0, 0 },
+	{ 0x3704, 0x8C30, 0 },
+	{ 0x3706, 0x3053, 0 },
+	{ 0x3708, 0x64F0, 0 },
+	{ 0x370A, 0xFC73, 0 },
+	{ 0x370C, 0xD311, 0 },
+	{ 0x370E, 0x336F, 0 },
+	{ 0x3710, 0x5AF3, 0 },
+	{ 0x3712, 0x4EAF, 0 },
+	{ 0x3714, 0xDBD4, 0 },
+
+	/* Lens shading origin */
+	{ 0x3644, 0x02A0, 0 },
+	{ 0x3642, 0x01FC, 0 },
+	{ 0x3210, 0x01B8, 0 },
+
+	/* Color correction matrix - Low light */
+	{ 0x098C, 0x2306, 0 },
+	{ 0x0990, 0x0233, 0 },
+	{ 0x098C, 0x2308, 0 },
+	{ 0x0990, 0xFF0B, 0 },
+	{ 0x098C, 0x230A, 0 },
+	{ 0x0990, 0x0024, 0 },
+	{ 0x098C, 0x230C, 0 },
+	{ 0x0990, 0xFFC8, 0 },
+	{ 0x098C, 0x230E, 0 },
+	{ 0x0990, 0x01DE, 0 },
+	{ 0x098C, 0x2310, 0 },
+	{ 0x0990, 0xFFBD, 0 },
+	{ 0x098C, 0x2312, 0 },
+	{ 0x0990, 0x0019, 0 },
+	{ 0x098C, 0x2314, 0 },
+	{ 0x0990, 0xFF2B, 0 },
+	{ 0x098C, 0x2316, 0 },
+	{ 0x0990, 0x01E8, 0 },
+	{ 0x098C, 0x2318, 0 },
+	{ 0x0990, 0x0024, 0 },
+	{ 0x098C, 0x231A, 0 },
+	{ 0x0990, 0x0030, 0 },
+
+	/* Color correction matrix - RL (delta) */
+	{ 0x098C, 0x231C, 0 },
+	{ 0x0990, 0xFF7D, 0 },
+	{ 0x098C, 0x231E, 0 },
+	{ 0x0990, 0x002C, 0 },
+	{ 0x098C, 0x2320, 0 },
+	{ 0x0990, 0x002C, 0 },
+	{ 0x098C, 0x2322, 0 },
+	{ 0x0990, 0x0006, 0 },
+	{ 0x098C, 0x2324, 0 },
+	{ 0x0990, 0x00A3, 0 },
+	{ 0x098C, 0x2326, 0 },
+	{ 0x0990, 0xFF75, 0 },
+	{ 0x098C, 0x2328, 0 },
+	{ 0x0990, 0xFFF4, 0 },
+	{ 0x098C, 0x232A, 0 },
+	{ 0x0990, 0x00AC, 0 },
+	{ 0x098C, 0x232C, 0 },
+	{ 0x0990, 0xFF75, 0 },
+	{ 0x098C, 0x232E, 0 },
+	{ 0x0990, 0x0010, 0 },
+	{ 0x098C, 0x2330, 0 },
+	{ 0x0990, 0xFFF4, 0 },
+
+	/* AWB settings */
+	{ 0x098C, 0xA348, 0 },
+	{ 0x0990, 0x0008, 0 },
+	{ 0x098C, 0xA349, 0 },
+	{ 0x0990, 0x0002, 0 },
+	{ 0x098C, 0xA34A, 0 },
+	{ 0x0990, 0x0059, 0 },
+	{ 0x098C, 0xA34B, 0 },
+	{ 0x0990, 0x00A6, 0 },
+	{ 0x098C, 0xA351, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0xA352, 0 },
+	{ 0x0990, 0x007F, 0 },
+	{ 0x098C, 0xA35D, 0 },
+	{ 0x0990, 0x0078, 0 },
+	{ 0x098C, 0xA35E, 0 },
+	{ 0x0990, 0x0086, 0 },
+	{ 0x098C, 0xA35F, 0 },
+	{ 0x0990, 0x007E, 0 },
+	{ 0x098C, 0xA360, 0 },
+	{ 0x0990, 0x0082, 0 },
+
+	/* Cold color adjustment */
+	{ 0x098C, 0xA369, 0 },
+	{ 0x0990, 0x0097, 0 },
+	{ 0x098C, 0xA36A, 0 },
+	{ 0x0990, 0x008C, 0 },
+	{ 0x098C, 0xA36B, 0 },
+	{ 0x0990, 0x0080, 0 },
+
+	/* AWB window */
+	{ 0x098C, 0xA302, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0xA303, 0 },
+	{ 0x0990, 0x00FF, 0 },
+
+	/* AE preview settings */
+	{ 0x098C, 0xA11D, 0 },
+	{ 0x0990, 0x0002, 0 },
+	{ 0x098C, 0x271F, 0 },
+	{ 0x0990, 0x032E, 0 },
+	{ 0x098C, 0x2721, 0 },
+	{ 0x0990, 0x04CC, 0 },
+
+	/* AE gain settings */
+	{ 0x098C, 0xA216, 0 },
+	{ 0x0990, 0x0060, 0 },
+	{ 0x098C, 0xA215, 0 },
+	{ 0x0990, 0x000A, 0 },
+	{ 0x098C, 0xA20C, 0 },
+	{ 0x0990, 0x0028, 0 },
+	{ 0x098C, 0xA24F, 0 },
+	{ 0x0990, 0x0042, 0 },
+	{ 0x098C, 0xA20E, 0 },
+	{ 0x0990, 0x0060, 0 },
+
+	/* AE window */
+	{ 0x098C, 0xA202, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0xA203, 0 },
+	{ 0x0990, 0x00FF, 0 },
+	{ 0x098C, 0xA207, 0 },
+	{ 0x0990, 0x0004, 0 },
+
+	/* Gamma morph control */
+	{ 0x098C, 0xAB37, 0 },
+	{ 0x0990, 0x0003, 0 },
+	{ 0x098C, 0x2B38, 0 },
+	{ 0x0990, 0x3A98, 0 },
+	{ 0x098C, 0x2B3A, 0 },
+	{ 0x0990, 0x5000, 0 },
+
+	/* Saturation */
+	{ 0x098C, 0xAB20, 0 },
+	{ 0x0990, 0x0023, 0 },
+	{ 0x098C, 0xAB24, 0 },
+	{ 0x0990, 0x0010, 0 },
+
+	/* AE speed */
+	{ 0x098C, 0xA109, 0 },
+	{ 0x0990, 0x0020, 0 },
+	{ 0x098C, 0xA10A, 0 },
+	{ 0x0990, 0x0002, 0 },
+
+	/* Gamma table A */
+	{ 0x098C, 0xAB3C, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0xAB3D, 0 },
+	{ 0x0990, 0x0006, 0 },
+	{ 0x098C, 0xAB3E, 0 },
+	{ 0x0990, 0x0014, 0 },
+	{ 0x098C, 0xAB3F, 0 },
+	{ 0x0990, 0x0038, 0 },
+	{ 0x098C, 0xAB40, 0 },
+	{ 0x0990, 0x005F, 0 },
+	{ 0x098C, 0xAB41, 0 },
+	{ 0x0990, 0x0079, 0 },
+	{ 0x098C, 0xAB42, 0 },
+	{ 0x0990, 0x008D, 0 },
+	{ 0x098C, 0xAB43, 0 },
+	{ 0x0990, 0x009E, 0 },
+	{ 0x098C, 0xAB44, 0 },
+	{ 0x0990, 0x00AC, 0 },
+	{ 0x098C, 0xAB45, 0 },
+	{ 0x0990, 0x00B8, 0 },
+	{ 0x098C, 0xAB46, 0 },
+	{ 0x0990, 0x00C3, 0 },
+	{ 0x098C, 0xAB47, 0 },
+	{ 0x0990, 0x00CD, 0 },
+	{ 0x098C, 0xAB48, 0 },
+	{ 0x0990, 0x00D5, 0 },
+	{ 0x098C, 0xAB49, 0 },
+	{ 0x0990, 0x00DE, 0 },
+	{ 0x098C, 0xAB4A, 0 },
+	{ 0x0990, 0x00E5, 0 },
+	{ 0x098C, 0xAB4B, 0 },
+	{ 0x0990, 0x00EC, 0 },
+	{ 0x098C, 0xAB4C, 0 },
+	{ 0x0990, 0x00F3, 0 },
+	{ 0x098C, 0xAB4D, 0 },
+	{ 0x0990, 0x00F9, 0 },
+	{ 0x098C, 0xAB4E, 0 },
+	{ 0x0990, 0x00FF, 0 },
+
+	/* Noise reduction RGB */
+	{ 0x098C, 0xAB2C, 0 },
+	{ 0x0990, 0x0010, 0 },
+	{ 0x098C, 0xAB2D, 0 },
+	{ 0x0990, 0x002A, 0 },
+	{ 0x098C, 0xAB2E, 0 },
+	{ 0x0990, 0x0010, 0 },
+	{ 0x098C, 0xAB2F, 0 },
+	{ 0x0990, 0x0010, 0 },
+
+	/* Gamma table B */
+	{ 0x098C, 0xAB4F, 0 },
+	{ 0x0990, 0x0000, 0 },
+	{ 0x098C, 0xAB50, 0 },
+	{ 0x0990, 0x0004, 0 },
+	{ 0x098C, 0xAB51, 0 },
+	{ 0x0990, 0x000D, 0 },
+	{ 0x098C, 0xAB52, 0 },
+	{ 0x0990, 0x0028, 0 },
+	{ 0x098C, 0xAB53, 0 },
+	{ 0x0990, 0x0053, 0 },
+	{ 0x098C, 0xAB54, 0 },
+	{ 0x0990, 0x0075, 0 },
+	{ 0x098C, 0xAB55, 0 },
+	{ 0x0990, 0x0092, 0 },
+	{ 0x098C, 0xAB56, 0 },
+	{ 0x0990, 0x00A7, 0 },
+	{ 0x098C, 0xAB57, 0 },
+	{ 0x0990, 0x00B7, 0 },
+	{ 0x098C, 0xAB58, 0 },
+	{ 0x0990, 0x00C4, 0 },
+	{ 0x098C, 0xAB59, 0 },
+	{ 0x0990, 0x00CF, 0 },
+	{ 0x098C, 0xAB5A, 0 },
+	{ 0x0990, 0x00D8, 0 },
+	{ 0x098C, 0xAB5B, 0 },
+	{ 0x0990, 0x00DF, 0 },
+	{ 0x098C, 0xAB5C, 0 },
+	{ 0x0990, 0x00E6, 0 },
+	{ 0x098C, 0xAB5D, 0 },
+	{ 0x0990, 0x00EC, 0 },
+	{ 0x098C, 0xAB5E, 0 },
+	{ 0x0990, 0x00F2, 0 },
+	{ 0x098C, 0xAB5F, 0 },
+	{ 0x0990, 0x00F6, 0 },
+	{ 0x098C, 0xAB60, 0 },
+	{ 0x0990, 0x00FB, 0 },
+	{ 0x098C, 0xAB61, 0 },
+	{ 0x0990, 0x00FF, 0 },
+
+	/* Read mode - no mirror/flip */
+	{ 0x098C, 0x2717, 0 },
+	{ 0x0990, 0x046C, 0 },
+	{ 0x098C, 0x272D, 0 },
+	{ 0x0990, 0x0024, 0 },
+
+	/* Reset command before sequencer */
+	{ 0x001A, 0x021C, 0 },
+
+	/* Issue refresh command */
+	{ 0x098C, 0xA103, 0 },
+	{ 0x0990, 0x0006, 0 },
+};
+
+/* Apply the MT9M113 initialization table */
+static int mt9m113_sensor_init(struct mt9m113 *sensor)
+{
+	struct device *dev = &sensor->client->dev;
+	int ret = 0;
+	unsigned int i;
+
+	/* MCU is about to be fully re-initialised, so any prior test-pattern
+	 * override is gone. Clear the bookkeeping so a subsequent
+	 * V4L2_CID_TEST_PATTERN=0 does not re-run the disable sequence;
+	 * also drop the cached pattern value so stream_on() doesn't
+	 * replay the apply after a runtime suspend/resume cycle that
+	 * already reset MCU state.
+	 */
+	sensor->test_pattern_active = false;
+	sensor->test_pattern_value = 0;
+
+	dev_dbg(dev, "MT9M113: applying init table (%zu entries)\n",
+		 ARRAY_SIZE(mt9m113_init_table));
+
+	/*
+	 * Suspend double buffer updates so all init table writes take effect
+	 * atomically at frame start. Per datasheet, R0x0248[15] inhibits
+	 * transfers from pending to live registers.
+	 */
+	ret = mt9m113_double_buffer_suspend(sensor);
+	if (ret) {
+		dev_warn(dev, "Failed to suspend double buffer: %d\n", ret);
+		/* Continue anyway - not fatal */
+	}
+
+	for (i = 0; i < ARRAY_SIZE(mt9m113_init_table); i++) {
+		const struct mt9m113_reg_entry *entry = &mt9m113_init_table[i];
+
+		ret = cci_write(sensor->regmap, CCI_REG16(entry->reg),
+				entry->value, NULL);
+		if (ret < 0) {
+			dev_err(dev, "MT9M113: reg 0x%04x write failed: %d\n",
+				entry->reg, ret);
+			mt9m113_double_buffer_resume(sensor);
+			return ret;
+		}
+
+		if (entry->delay_ms > 0)
+			msleep(entry->delay_ms);
+	}
+
+	/* Resume double buffer updates - changes take effect at next frame start */
+	ret = mt9m113_double_buffer_resume(sensor);
+	if (ret) {
+		dev_warn(dev, "Failed to resume double buffer: %d\n", ret);
+		/* Continue anyway - not fatal */
+	}
+
+	/* Wait for MCU to complete refresh */
+	ret = mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 1000);
+	if (ret < 0) {
+		dev_err(dev, "MT9M113: MCU refresh timeout\n");
+		return ret;
+	}
+
+	/* Issue sequencer refresh */
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CMD,
+				    MT9M113_SEQ_CMD_REFRESH);
+	if (ret < 0)
+		return ret;
+
+	ret = mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 1000);
+	if (ret < 0) {
+		dev_err(dev, "MT9M113: SEQ refresh timeout\n");
+		return ret;
+	}
+
+	/*
+	 * Enable MIPI Frame-Start/End short packets. Without them the VFE CAMIF
+	 * has no per-frame boundary marker and runs in line-counting (APS) mode,
+	 * which slips against the sensor's free-running readout and makes the
+	 * captured image walk vertically (~tens of lines/frame, resolution
+	 * dependent). Emitting the frame-sync short packets locks every frame.
+	 */
+	ret = cci_write(sensor->regmap, MT9M113_CUSTOM_SHORT_PKT,
+			MT9M113_CUSTOM_SHORT_PKT_FRAME_CNT_EN, NULL);
+	if (ret < 0)
+		return ret;
+
+	/*
+	 * Do NOT enable MIPI output here. It is enabled in start_streaming once
+	 * the output config is programmed, followed by REFRESH_MODE + REFRESH.
+	 * Enabling MIPI before the CSIPHY is configured causes problems.
+	 */
+
+	dev_dbg(dev, "MT9M113: init complete\n");
+	return 0;
+}
+
+/* -----------------------------------------------------------------------------
+ * Streaming
+ */
+
+/*
+ * Configure AE (Auto Exposure) parameters for preview vs snapshot/video mode.
+ * From legacy vendor driver: snapshot mode allows longer exposure for better quality,
+ * while preview mode optimizes for higher frame rate and lower power.
+ */
+static int mt9m113_configure_ae_mode(struct mt9m113 *sensor, bool snapshot_mode)
+{
+	struct device *dev = &sensor->client->dev;
+	int ret;
+
+	if (snapshot_mode) {
+		/* Snapshot/Capture mode: allow longer exposure for quality */
+		dev_dbg(dev, "MT9M113: configuring AE for snapshot mode\n");
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_MAX_INDEX, 0x0028);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_MAX_VIRTGAIN, 0x0060);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_MAX_DGAIN_AE1, 0x00C8);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_JUMP_DIVISOR, 0x0002);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_SKIP_FRAMES, 0x0002);
+		if (ret)
+			return ret;
+	} else {
+		/* Preview mode: optimize for frame rate and responsiveness */
+		dev_dbg(dev, "MT9M113: configuring AE for preview mode\n");
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_MAX_INDEX, 0x0008);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_MAX_VIRTGAIN, 0x00A0);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_MAX_DGAIN_AE1, 0x0150);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_JUMP_DIVISOR, 0x0001);
+		if (ret)
+			return ret;
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_SKIP_FRAMES, 0x0001);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+/*
+ * Program the per-context output dimensions and pixel format, then re-apply the
+ * sensor readout geometry. Output configuration is double-buffered so it
+ * latches atomically at the next frame start.
+ */
+static int mt9m113_configure_output(struct mt9m113 *sensor, bool use_context_b,
+				    u32 code)
+{
+	struct device *dev = &sensor->client->dev;
+	u16 width_reg, height_reg, format_reg;
+	u16 width_val, height_val, format_val;
+	int ret;
+
+	if (use_context_b) {
+		width_reg = MT9M113_MODE_OUTPUT_WIDTH_B;
+		height_reg = MT9M113_MODE_OUTPUT_HEIGHT_B;
+		format_reg = MT9M113_MODE_OUTPUT_FORMAT_B;
+		width_val = 1280;
+		height_val = 1024;
+	} else {
+		width_reg = MT9M113_MODE_OUTPUT_WIDTH_A;
+		height_reg = MT9M113_MODE_OUTPUT_HEIGHT_A;
+		format_reg = MT9M113_MODE_OUTPUT_FORMAT_A;
+		width_val = 640;
+		height_val = 480;
+	}
+
+	format_val = mt9m113_format_val(code);
+
+	ret = mt9m113_double_buffer_suspend(sensor);
+	if (ret)
+		dev_warn(dev, "Failed to suspend double buffer: %d\n", ret);
+
+	ret = mt9m113_write_mcu_var(sensor, width_reg, width_val);
+	if (!ret)
+		ret = mt9m113_write_mcu_var(sensor, height_reg, height_val);
+	if (!ret)
+		ret = mt9m113_write_mcu_var(sensor, format_reg, format_val);
+
+	{
+		int resume_ret = mt9m113_double_buffer_resume(sensor);
+
+		if (!ret)
+			ret = resume_ret;
+	}
+	if (ret)
+		return ret;
+
+	dev_dbg(dev, "MT9M113: Context %c: %ux%u, format=0x%04x\n",
+		use_context_b ? 'B' : 'A', width_val, height_val, format_val);
+
+	/*
+	 * Re-apply the sensor readout geometry. The MCU variables for
+	 * row_end/read_mode can be left stale after switching from Context B to
+	 * Context A, producing wrong line counts and a corrupted image.
+	 */
+	ret = mt9m113_configure_sensor_context(sensor, use_context_b);
+	if (ret)
+		dev_err(dev, "Failed to configure sensor context: %d\n", ret);
+
+	return ret;
+}
+
+/*
+ * The MCU resets OUTPUT_CONTROL and MODE_OUTPUT_FORMAT to their defaults during
+ * sequencer state transitions, so re-apply them once the stream is running.
+ * Best-effort: a failure is logged but does not abort the stream.
+ */
+static void mt9m113_reassert_output(struct mt9m113 *sensor, u16 output_ctrl_val,
+				    u16 format_reg, u32 code)
+{
+	struct device *dev = &sensor->client->dev;
+	u16 format_val = mt9m113_format_val(code);
+
+	if (cci_write(sensor->regmap, MT9M113_OUTPUT_CONTROL, output_ctrl_val,
+		      NULL))
+		dev_warn(dev, "MT9M113: OUTPUT_CONTROL re-write failed\n");
+	if (mt9m113_write_mcu_var(sensor, format_reg, format_val))
+		dev_warn(dev, "MT9M113: MODE_OUTPUT_FORMAT re-write failed\n");
+}
+
+/* Context A (640x480 preview): enable output and run the preview sequencer. */
+static int mt9m113_stream_context_a(struct mt9m113 *sensor, u16 output_ctrl_val,
+				    u32 code)
+{
+	struct device *dev = &sensor->client->dev;
+	int ret;
+
+	ret = cci_write(sensor->regmap, MT9M113_OUTPUT_CONTROL, output_ctrl_val,
+			NULL);
+	if (ret)
+		return ret;
+	dev_dbg(dev, "MT9M113: OUTPUT_CONTROL=0x%04x enabled\n", output_ctrl_val);
+
+	ret = cci_write(sensor->regmap, MT9M113_RESET_REGISTER,
+			MT9M113_RESET_REG_STREAMING, NULL);
+	if (ret)
+		return ret;
+
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CAP_MODE,
+				    MT9M113_SEQ_CAP_MODE_PREVIEW);
+	if (ret)
+		return ret;
+	msleep(40);
+
+	dev_dbg(dev, "MT9M113: Writing SEQ_CMD_RUN\n");
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CMD, MT9M113_SEQ_CMD_RUN);
+	if (ret)
+		return ret;
+
+	/*
+	 * Wait for the sequencer to ack SEQ_CMD_RUN by clearing SEQ_CMD.
+	 * A timeout here means the MCU is wedged (probably stuck in the
+	 * intermittent stream-start wedge described in the comment above
+	 * mt9m113_start_streaming); propagate the error so the retry loop
+	 * actually triggers a power-cycle. Silently returning 0 would
+	 * leave the caller thinking the stream came up cleanly and the
+	 * wedge would persist until userspace gives up.
+	 */
+	ret = mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 500);
+	if (ret < 0) {
+		dev_warn(dev, "MT9M113: SEQ_CMD_RUN did not complete\n");
+		return ret;
+	}
+
+	mt9m113_reassert_output(sensor, output_ctrl_val,
+				MT9M113_MODE_OUTPUT_FORMAT_A, code);
+	msleep(20);
+	return 0;
+}
+
+/*
+ * Context B (1280x1024 capture): the MCU only accepts CAPTURE from stable
+ * preview, so enter preview first, wait for SEQ_STATE=PREVIEW, then switch.
+ */
+static int mt9m113_stream_context_b(struct mt9m113 *sensor, u16 output_ctrl_val,
+				    u32 code)
+{
+	struct device *dev = &sensor->client->dev;
+	u64 seq_state = 0;
+	unsigned int i;
+	int ret;
+
+	ret = cci_write(sensor->regmap, MT9M113_OUTPUT_CONTROL, output_ctrl_val,
+			NULL);
+	if (ret)
+		return ret;
+	dev_dbg(dev, "MT9M113: OUTPUT_CONTROL=0x%04x enabled\n", output_ctrl_val);
+
+	ret = cci_write(sensor->regmap, MT9M113_RESET_REGISTER,
+			MT9M113_RESET_REG_STREAMING, NULL);
+	if (ret)
+		return ret;
+
+	/* Enter preview first. */
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CAP_MODE,
+				    MT9M113_SEQ_CAP_MODE_PREVIEW);
+	if (ret)
+		return ret;
+	msleep(40);
+
+	dev_dbg(dev, "MT9M113: Entering preview mode first (SEQ_CMD_RUN)\n");
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CMD, MT9M113_SEQ_CMD_RUN);
+	if (ret)
+		return ret;
+
+	ret = mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 500);
+	if (ret < 0)
+		dev_warn(dev, "MT9M113: SEQ_CMD_RUN did not complete\n");
+
+	for (i = 0; i < 100; i++) {
+		mt9m113_read_mcu_var(sensor, MT9M113_SEQ_STATE, &seq_state);
+		if (seq_state == MT9M113_SEQ_STATE_PREVIEW)
+			break;
+		usleep_range(10000, 11000);
+	}
+	if (seq_state != MT9M113_SEQ_STATE_PREVIEW)
+		dev_warn(dev,
+			 "MT9M113: preview state not reached (SEQ_STATE=0x%llx)\n",
+			 seq_state);
+
+	/* Switch to video mode (stays in Context B). */
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CAP_MODE,
+				    MT9M113_SEQ_CAP_MODE_VIDEO);
+	if (ret)
+		return ret;
+	msleep(40);
+
+	ret = mt9m113_seq_cmd_ready(sensor);
+	if (ret < 0) {
+		dev_err(dev, "MT9M113: MCU not ready for CAPTURE command\n");
+		return ret;
+	}
+
+	dev_dbg(dev, "MT9M113: switching to Context B (SEQ_CMD_CAPTURE)\n");
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CMD,
+				    MT9M113_SEQ_CMD_CAPTURE);
+	if (ret)
+		return ret;
+
+	ret = mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 500);
+	if (ret < 0)
+		dev_warn(dev, "MT9M113: SEQ_CMD_CAPTURE did not complete\n");
+
+	for (i = 0; i < 100; i++) {
+		mt9m113_read_mcu_var(sensor, MT9M113_SEQ_STATE, &seq_state);
+		if (seq_state == MT9M113_SEQ_STATE_CAPTURE)
+			break;
+		usleep_range(10000, 11000);
+	}
+	if (seq_state != MT9M113_SEQ_STATE_CAPTURE) {
+		dev_err(dev, "MT9M113: Context B switch failed (SEQ_STATE=0x%llx)\n",
+			seq_state);
+		return -ETIMEDOUT;
+	}
+	dev_dbg(dev, "MT9M113: reached capture state\n");
+
+	mt9m113_reassert_output(sensor, output_ctrl_val,
+				MT9M113_MODE_OUTPUT_FORMAT_B, code);
+
+	/* The sensor pipeline needs time to reconfigure for 1280x1024. */
+	msleep(200);
+	return 0;
+}
+
+/*
+ * Apply the MCU-side test-pattern config and freeze the MCU in boot mode
+ * so the normal sequencer cannot overwrite it. Per the MT9M113 datasheet
+ * R0x0066 (mode_common_mode_settings_test_mode) "Test Pattern Generator.
+ * Changes take effect only after REFRESH command", and "Disabling the MCU
+ * is recommended before enabling test patterns."
+ *
+ * The pattern index is written verbatim to the Driver-7 R0x0066 register
+ * (encoded as MT9M113_MODE_TEST_MODE = 0x2766) - menu items already match
+ * the datasheet's value table 1..5. NOTE: do NOT switch CAM_MODE_SELECT
+ * into a "test pattern" state - that puts the camera-mode driver into a
+ * mode where the sequencer no longer processes SEQ_CMD, and the REFRESH
+ * below hangs.
+ *
+ * Called from two places:
+ *
+ *   1. mt9m113_s_ctrl(V4L2_CID_TEST_PATTERN > 0) while streaming - apply
+ *      immediately.
+ *
+ *   2. mt9m113_stream_on(), after stream_context_{a,b}() has brought the
+ *      pipeline up - re-apply when the test pattern was selected before
+ *      streaming started. __v4l2_ctrl_handler_setup() in stream_on runs
+ *      with sensor->streaming still false, so the in-handler halt is
+ *      skipped; replaying here makes the "set test pattern, then STREAMON"
+ *      sequence work.
+ *
+ * Caller must hold the same context as mt9m113_s_ctrl() (an active PM
+ * reference); we touch MCU vars + BOOT_MODE.
+ */
+static int mt9m113_apply_test_pattern(struct mt9m113 *sensor, u8 pattern_idx)
+{
+	int ret;
+
+	ret = mt9m113_write_mcu_var(sensor, MT9M113_MODE_TEST_MODE, pattern_idx);
+	if (ret)
+		return ret;
+	ret = mt9m113_refresh(sensor);
+	if (ret)
+		return ret;
+
+	dev_dbg(&sensor->client->dev,
+		"MT9M113: Halting MCU to lock test pattern %u\n", pattern_idx);
+	cci_write(sensor->regmap, MT9M113_MCU_BOOT_MODE, 0x0001, &ret);
+	return ret;
+}
+
+static int mt9m113_stream_on(struct mt9m113 *sensor,
+			     struct v4l2_subdev_state *state)
+{
+	struct device *dev = &sensor->client->dev;
+	const struct v4l2_mbus_framefmt *format;
+	const struct v4l2_rect *compose;
+	u64 health_check, seq_cmd;
+	bool use_context_b;
+	u16 output_ctrl_val;
+	int ret;
+
+	/*
+	 * Wake the MCU out of standby and give it time to settle (50ms matches
+	 * the legacy vendor driver).
+	 */
+	dev_dbg(dev, "MT9M113: start_streaming\n");
+	cci_write(sensor->regmap, MT9M113_STANDBY_CONTROL,
+		  MT9M113_STANDBY_CONTROL_ACTIVE, NULL);
+	msleep(50);
+
+	/*
+	 * MCU health check: MODE_OUTPUT_WIDTH_A reads back non-zero once the
+	 * MCU is running. If it is unresponsive, re-apply the init table.
+	 */
+	ret = mt9m113_read_mcu_var(sensor, MT9M113_MODE_OUTPUT_WIDTH_A,
+				   &health_check);
+	if (ret < 0 || health_check == 0) {
+		dev_warn(dev, "MT9M113: MCU unresponsive, re-init\n");
+		ret = mt9m113_sensor_init(sensor);
+		if (ret < 0)
+			return ret;
+	}
+
+	/*
+	 * Clear stale SEQ_CMD. The init table ends with REFRESH (SEQ_CMD=0x05)
+	 * which may not complete before the sensor enters standby/suspend. On
+	 * resume, SEQ_CMD is stuck at 0x05 and all subsequent commands fail.
+	 * Force-clear it.
+	 */
+	ret = mt9m113_read_mcu_var(sensor, MT9M113_SEQ_CMD, &seq_cmd);
+	if (ret == 0 && seq_cmd != 0) {
+		dev_warn(dev, "MT9M113: Clearing stale SEQ_CMD=0x%llx\n", seq_cmd);
+		mt9m113_write_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000);
+		msleep(50);
+	}
+
+	/*
+	 * Re-assert the MIPI Frame-Start/End short packets on every stream
+	 * start. The MCU clears CUSTOM_SHORT_PKT across standby/refresh
+	 * cycles, and mt9m113_sensor_init() (which also sets it) only runs on
+	 * the MCU-recovery path - so without this the bit is lost after the
+	 * first stream. Without the per-frame boundary marker the VFE CAMIF
+	 * falls back to line-counting and the image walks vertically.
+	 */
+	ret = cci_write(sensor->regmap, MT9M113_CUSTOM_SHORT_PKT,
+			MT9M113_CUSTOM_SHORT_PKT_FRAME_CNT_EN, NULL);
+	if (ret < 0)
+		return ret;
+
+	/* Apply all V4L2 controls (color effects, etc.) before streaming */
+	ret = __v4l2_ctrl_handler_setup(&sensor->ifp.hdl);
+	if (ret) {
+		dev_err(dev, "Failed to setup controls: %d\n", ret);
+		return ret;
+	}
+
+	compose = v4l2_subdev_state_get_compose(state, 0);
+	/* Source pad (pad 1) carries the MIPI output format. */
+	format = v4l2_subdev_state_get_format(state, 1);
+
+	use_context_b = (compose->width > 640 || compose->height > 480);
+	dev_dbg(dev, "MT9M113: %ux%u -> Context %c\n",
+		compose->width, compose->height, use_context_b ? 'B' : 'A');
+
+	/*
+	 * Configure AE for the selected context: Context B (capture) allows a
+	 * longer exposure for quality, Context A (preview) optimises for frame
+	 * rate.
+	 */
+	ret = mt9m113_configure_ae_mode(sensor, use_context_b);
+	if (ret) {
+		dev_err(dev, "Failed to configure AE mode: %d\n", ret);
+		return ret;
+	}
+
+	ret = mt9m113_configure_output(sensor, use_context_b, format->code);
+	if (ret)
+		return ret;
+
+	/* Wait for CSIPHY stabilization */
+	msleep(MT9M113_PRE_MIPI_DELAY_MS);
+
+	/*
+	 * YUV/RGB output: enable MIPI CSI-2 in FIFO-bypass mode and restore the
+	 * default ISP colour pipeline.
+	 */
+	output_ctrl_val = MT9M113_OUTPUT_CONTROL_MIPI_ENABLE;
+	ret = cci_write(sensor->regmap, MT9M113_OFIFO_CONTROL_STATUS,
+			MT9M113_OFIFO_BYPASS, NULL);
+	if (ret)
+		return ret;
+	ret = cci_write(sensor->regmap, MT9M113_COLOR_PIPELINE_CONTROL,
+			MT9M113_COLOR_PIPELINE_DEFAULT, NULL);
+	if (ret)
+		return ret;
+
+	/* The MCU must be idle before it will accept a sequencer command. */
+	ret = mt9m113_seq_cmd_ready(sensor);
+	if (ret < 0) {
+		dev_err(dev, "MT9M113: MCU not ready for streaming command\n");
+		return ret;
+	}
+
+	/*
+	 * One REFRESH applies the per-session output dimensions/format; without
+	 * it SEQ_CMD_RUN hangs (the MCU never clears SEQ_CMD). OFIFO and the
+	 * colour pipeline are already programmed above - they must not be
+	 * written while a REFRESH is pending or it times out.
+	 */
+	ret = mt9m113_refresh(sensor);
+	if (ret)
+		dev_warn(dev, "MT9M113: REFRESH failed, continuing\n");
+
+	/*
+	 * For Context B the MCU must reach stable preview (SEQ_STATE=0x04)
+	 * before it will accept the CAPTURE command, so enter preview first and
+	 * then switch. Context A simply runs preview.
+	 */
+	if (use_context_b)
+		ret = mt9m113_stream_context_b(sensor, output_ctrl_val,
+					       format->code);
+	else
+		ret = mt9m113_stream_context_a(sensor, output_ctrl_val,
+					       format->code);
+	if (ret)
+		return ret;
+
+	/*
+	 * If V4L2_CID_TEST_PATTERN was set to a non-zero value before the
+	 * stream started, mt9m113_s_ctrl() noted it on test_pattern_{active,
+	 * value} but couldn't commit the MCU writes (the normal stream init
+	 * below would have overwritten MODE_SELECT). Replay the apply now
+	 * that the pipeline is up so STREAMON ends with the test pattern
+	 * visible. Read the cached value directly - do NOT call
+	 * v4l2_ctrl_find() here, the ctrl-handler mutex is already held by
+	 * the V4L2 core for the duration of this s_stream callback, so a
+	 * lookup would self-deadlock.
+	 */
+	if (sensor->test_pattern_active && sensor->test_pattern_value > 0) {
+		ret = mt9m113_apply_test_pattern(sensor,
+						 sensor->test_pattern_value);
+		if (ret) {
+			/*
+			 * Replay failed - the chip is still in its normal
+			 * streaming state. Clear the cached test-pattern
+			 * flags so the !test_pattern_active guards in
+			 * subsequent s_ctrl() refresh paths do not get
+			 * stuck thinking the MCU is halted.
+			 */
+			sensor->test_pattern_active = false;
+			sensor->test_pattern_value = 0;
+			return ret;
+		}
+	}
+
+	dev_dbg(dev, "MT9M113: streaming started\n");
+	return 0;
+}
+
+static int mt9m113_power_on(struct mt9m113 *sensor);
+static void mt9m113_power_off(struct mt9m113 *sensor);
+
+/*
+ * The MT9M113 MCU intermittently wedges on stream start; the failures cluster
+ * at the start of a run and then clear. Retry a bounded number of times,
+ * power-cycling the sensor (power_off + power_on + sensor_init) between
+ * attempts.
+ *
+ * The V4L2 bridge consumer holds a managed PM device-link reference
+ * (DL_FLAG_PM_RUNTIME) on this sensor for the duration of s_stream, so a
+ * pm_runtime_suspend() between attempts would be rejected with -EBUSY and the
+ * MCU could never actually power-cycle through the standard runtime-PM path.
+ * Take exactly one PM reference for the whole retry loop and toggle
+ * power_off/power_on directly between attempts; on success the reference is
+ * held for the streaming session and released by mt9m113_stop_streaming().
+ * On terminal failure sync the PM state to SUSPENDED so a future resume
+ * re-runs power_on instead of skipping it.
+ */
+static int mt9m113_start_streaming(struct mt9m113 *sensor,
+				   struct v4l2_subdev_state *state)
+	__must_hold(state->lock)
+{
+	struct device *dev = &sensor->client->dev;
+	unsigned int attempt;
+	int ret;
+
+	/*
+	 * Guard against a redundant s_stream(1): a second
+	 * pm_runtime_resume_and_get() here would leak a PM reference and pin
+	 * the device awake forever, because mt9m113_stop_streaming() only
+	 * drops one reference per stop.
+	 */
+	if (sensor->streaming)
+		return 0;
+
+	ret = pm_runtime_resume_and_get(dev);
+	if (ret)
+		return ret;
+
+	for (attempt = 0; attempt < MT9M113_STREAM_START_RETRIES; attempt++) {
+		ret = mt9m113_stream_on(sensor, state);
+		if (!ret) {
+			sensor->streaming = true;
+			return 0;
+		}
+
+		if (attempt + 1 >= MT9M113_STREAM_START_RETRIES) {
+			/*
+			 * Final attempt failed with the chip still powered up
+			 * from mt9m113_stream_on(). Bring it down once before
+			 * syncing PM state so the runtime_status=SUSPENDED
+			 * below matches the hardware.
+			 */
+			mt9m113_power_off(sensor);
+			break;
+		}
+
+		dev_warn(dev,
+			 "MT9M113: stream start failed (%d), power-cycle + retry %u/%u\n",
+			 ret, attempt + 1, MT9M113_STREAM_START_RETRIES - 1);
+
+		mt9m113_power_off(sensor);
+		ret = mt9m113_power_on(sensor);
+		if (ret)
+			/* chip is already off after the failed power_on */
+			break;
+		ret = mt9m113_sensor_init(sensor);
+		if (ret) {
+			mt9m113_power_off(sensor);
+			break;
+		}
+	}
+
+	/*
+	 * Every break above leaves the chip powered off. The PM core is still
+	 * in RPM_ACTIVE (we bypassed the suspend callback all the way through),
+	 * and calling pm_runtime_set_suspended() here would fail with -EAGAIN
+	 * while pm_runtime is enabled -- leaving PM ACTIVE while the chip is
+	 * off. The next autosuspend would then fire mt9m113_runtime_suspend()
+	 * and call mt9m113_power_off() a second time, underflowing the clk
+	 * and regulator refcounts.
+	 *
+	 * Record the chip-off fact on the sensor; runtime_suspend consumes it
+	 * to skip its own power_off, keeping clk / regulator refcounts
+	 * balanced. Drop the PM reference via put_noidle so we don't kick an
+	 * autosuspend immediately; the bridge consumer's device-link reference
+	 * will keep PM ACTIVE anyway until the bridge tears down.
+	 */
+	sensor->chip_off = true;
+	pm_runtime_put_noidle(dev);
+	return ret;
+}
+
+static int mt9m113_stop_streaming(struct mt9m113 *sensor)
+{
+	struct device *dev = &sensor->client->dev;
+	int ret;
+
+	if (!sensor->streaming)
+		return 0;
+
+	sensor->streaming = false;
+
+	/* Disable MIPI output */
+	ret = cci_write(sensor->regmap, MT9M113_OUTPUT_CONTROL, 0x0000, NULL);
+	if (ret < 0)
+		dev_warn(dev,
+			 "MT9M113: MIPI output disable failed (%d), receiver may still see traffic\n",
+			 ret);
+
+	/*
+	 * Wait briefly for any pending SEQ_CMD to complete.
+	 * Don't issue REFRESH here - it can leave MCU stuck if it times out.
+	 * Start_streaming will handle full re-initialization.
+	 */
+	ret = mt9m113_poll_mcu_var(sensor, MT9M113_SEQ_CMD, 0x0000, 100);
+	if (ret < 0)
+		dev_dbg(dev, "MT9M113: SEQ_CMD did not complete before stop\n");
+
+	dev_dbg(dev, "MT9M113: streaming stopped\n");
+
+	pm_runtime_put_autosuspend(dev);
+	return 0;
+}
+
+/* -----------------------------------------------------------------------------
+ * V4L2 Subdev Operations
+ */
+
+static inline struct mt9m113 *ifp_to_mt9m113(struct v4l2_subdev *sd)
+{
+	return container_of(sd, struct mt9m113, ifp.sd);
+}
+
+static inline struct mt9m113 *pa_to_mt9m113(struct v4l2_subdev *sd)
+{
+	return container_of(sd, struct mt9m113, pa.sd);
+}
+
+static int mt9m113_ifp_s_stream(struct v4l2_subdev *sd, int enable)
+{
+	struct mt9m113 *sensor = ifp_to_mt9m113(sd);
+	struct v4l2_subdev_state *state;
+	int ret;
+
+	state = v4l2_subdev_lock_and_get_active_state(sd);
+
+	if (enable)
+		ret = mt9m113_start_streaming(sensor, state);
+	else
+		ret = mt9m113_stop_streaming(sensor);
+
+	v4l2_subdev_unlock_state(state);
+	return ret;
+}
+
+/* -----------------------------------------------------------------------------
+ * Pixel Array Subdev Operations
+ */
+
+static int mt9m113_pa_init_state(struct v4l2_subdev *sd,
+				 struct v4l2_subdev_state *state)
+{
+	struct v4l2_mbus_framefmt *format;
+	struct v4l2_rect *crop;
+
+	crop = v4l2_subdev_state_get_crop(state, 0);
+	crop->left = 0;
+	crop->top = 0;
+	crop->width = MT9M113_PIXEL_ARRAY_WIDTH;
+	crop->height = MT9M113_PIXEL_ARRAY_HEIGHT;
+
+	format = v4l2_subdev_state_get_format(state, 0);
+	format->width = MT9M113_PIXEL_ARRAY_WIDTH;
+	format->height = MT9M113_PIXEL_ARRAY_HEIGHT;
+	format->code = MEDIA_BUS_FMT_SGRBG10_1X10;
+	format->field = V4L2_FIELD_NONE;
+	format->colorspace = V4L2_COLORSPACE_RAW;
+
+	return 0;
+}
+
+static int mt9m113_pa_enum_mbus_code(struct v4l2_subdev *sd,
+				     struct v4l2_subdev_state *state,
+				     struct v4l2_subdev_mbus_code_enum *code)
+{
+	if (code->index > 0)
+		return -EINVAL;
+
+	code->code = MEDIA_BUS_FMT_SGRBG10_1X10;
+	return 0;
+}
+
+static int mt9m113_pa_enum_framesizes(struct v4l2_subdev *sd,
+				      struct v4l2_subdev_state *state,
+				      struct v4l2_subdev_frame_size_enum *fse)
+{
+	if (fse->index > 0)
+		return -EINVAL;
+
+	if (fse->code != MEDIA_BUS_FMT_SGRBG10_1X10)
+		return -EINVAL;
+
+	fse->min_width = MT9M113_PIXEL_ARRAY_WIDTH;
+	fse->max_width = MT9M113_PIXEL_ARRAY_WIDTH;
+	fse->min_height = MT9M113_PIXEL_ARRAY_HEIGHT;
+	fse->max_height = MT9M113_PIXEL_ARRAY_HEIGHT;
+
+	return 0;
+}
+
+static int mt9m113_pa_get_selection(struct v4l2_subdev *sd,
+				    struct v4l2_subdev_state *state,
+				    struct v4l2_subdev_selection *sel)
+{
+	switch (sel->target) {
+	case V4L2_SEL_TGT_CROP:
+		sel->r = *v4l2_subdev_state_get_crop(state, sel->pad);
+		return 0;
+
+	case V4L2_SEL_TGT_CROP_DEFAULT:
+	case V4L2_SEL_TGT_CROP_BOUNDS:
+	case V4L2_SEL_TGT_NATIVE_SIZE:
+		sel->r.left = 0;
+		sel->r.top = 0;
+		sel->r.width = MT9M113_PIXEL_ARRAY_WIDTH;
+		sel->r.height = MT9M113_PIXEL_ARRAY_HEIGHT;
+		return 0;
+
+	default:
+		return -EINVAL;
+	}
+}
+
+static const struct v4l2_subdev_pad_ops mt9m113_pa_pad_ops = {
+	.enum_mbus_code = mt9m113_pa_enum_mbus_code,
+	.enum_frame_size = mt9m113_pa_enum_framesizes,
+	.get_fmt = v4l2_subdev_get_fmt,
+	.get_selection = mt9m113_pa_get_selection,
+};
+
+static const struct v4l2_subdev_ops mt9m113_pa_ops = {
+	.pad = &mt9m113_pa_pad_ops,
+};
+
+/*
+ * v4l2_async_unregister_subdev() removes the /dev/v4l-subdev* node but does
+ * NOT block waiting for in-flight ioctls to complete; freeing the control
+ * handler (whose mutex an in-flight VIDIOC_S_CTRL may be holding) or kfree'ing
+ * the sensor struct directly from remove() would leave that ioctl unlocking
+ * freed memory. Defer per-subdev cleanup (subdev state, ctrl handler, media
+ * entity) and the kfree of the surrounding sensor struct until the v4l2
+ * framework fires the per-subdev release callback after the last fd on that
+ * devnode closes. release_count gates the kfree until BOTH subdevs' release
+ * callbacks have run, since PA and IFP both publish HAS_DEVNODE.
+ */
+static void mt9m113_release_sensor(struct mt9m113 *sensor)
+{
+	if (atomic_dec_and_test(&sensor->release_count)) {
+		/*
+		 * link_frequencies in bus_cfg is referenced by the IFP control
+		 * handler's qmenu_int (V4L2_CID_LINK_FREQ menu). Only safe to
+		 * free here, after both subdev .release callbacks have freed
+		 * their respective ctrl handlers.
+		 */
+		v4l2_fwnode_endpoint_free(&sensor->bus_cfg);
+		kfree(sensor);
+	}
+}
+
+static void mt9m113_pa_release(struct v4l2_subdev *sd)
+{
+	struct mt9m113 *sensor = pa_to_mt9m113(sd);
+
+	v4l2_subdev_cleanup(&sensor->pa.sd);
+	v4l2_ctrl_handler_free(&sensor->pa.hdl);
+	media_entity_cleanup(&sensor->pa.sd.entity);
+	mt9m113_release_sensor(sensor);
+}
+
+static const struct v4l2_subdev_internal_ops mt9m113_pa_internal_ops = {
+	.init_state = mt9m113_pa_init_state,
+	.release = mt9m113_pa_release,
+};
+
+/* -----------------------------------------------------------------------------
+ * IFP Subdev Operations
+ */
+
+static int mt9m113_ifp_init_state(struct v4l2_subdev *sd,
+				  struct v4l2_subdev_state *state)
+{
+	struct v4l2_mbus_framefmt *format;
+	struct v4l2_rect *crop;
+	struct v4l2_rect *compose;
+
+	/* Sink pad - receives raw data from PA */
+	format = v4l2_subdev_state_get_format(state, 0);
+	format->width = MT9M113_PIXEL_ARRAY_WIDTH;
+	format->height = MT9M113_PIXEL_ARRAY_HEIGHT;
+	format->code = MEDIA_BUS_FMT_SGRBG10_1X10;
+	format->field = V4L2_FIELD_NONE;
+	format->colorspace = V4L2_COLORSPACE_RAW;
+
+	crop = v4l2_subdev_state_get_crop(state, 0);
+	crop->left = 0;
+	crop->top = 0;
+	crop->width = MT9M113_PIXEL_ARRAY_WIDTH;
+	crop->height = MT9M113_PIXEL_ARRAY_HEIGHT;
+
+	compose = v4l2_subdev_state_get_compose(state, 0);
+	compose->left = 0;
+	compose->top = 0;
+	compose->width = 640;
+	compose->height = 480;
+
+	/* Source pad - outputs processed data to host */
+	format = v4l2_subdev_state_get_format(state, 1);
+	format->width = 640;
+	format->height = 480;
+	format->code = MEDIA_BUS_FMT_UYVY8_1X16;
+	format->field = V4L2_FIELD_NONE;
+	format->colorspace = V4L2_COLORSPACE_SRGB;
+
+	return 0;
+}
+
+static int mt9m113_ifp_enum_mbus_code(struct v4l2_subdev *sd,
+				      struct v4l2_subdev_state *state,
+				      struct v4l2_subdev_mbus_code_enum *code)
+{
+	/* Sink pad only accepts raw from PA */
+	if (code->pad == 0) {
+		if (code->index > 0)
+			return -EINVAL;
+		code->code = MEDIA_BUS_FMT_SGRBG10_1X10;
+		return 0;
+	}
+
+	/* Source pad supports multiple output formats */
+	if (code->index >= ARRAY_SIZE(mt9m113_format_infos))
+		return -EINVAL;
+
+	code->code = mt9m113_format_infos[code->index].code;
+	return 0;
+}
+
+static int mt9m113_ifp_enum_frame_size(struct v4l2_subdev *sd,
+				       struct v4l2_subdev_state *state,
+				       struct v4l2_subdev_frame_size_enum *fse)
+{
+	/* Sink pad - fixed raw input size */
+	if (fse->pad == 0) {
+		if (fse->index > 0)
+			return -EINVAL;
+		if (fse->code != MEDIA_BUS_FMT_SGRBG10_1X10)
+			return -EINVAL;
+		fse->min_width = MT9M113_PIXEL_ARRAY_WIDTH;
+		fse->max_width = MT9M113_PIXEL_ARRAY_WIDTH;
+		fse->min_height = MT9M113_PIXEL_ARRAY_HEIGHT;
+		fse->max_height = MT9M113_PIXEL_ARRAY_HEIGHT;
+		return 0;
+	}
+
+	/* Source pad - Context A (640x480) and Context B (1280x1024) */
+	if (fse->index > 1)
+		return -EINVAL;
+	if (mt9m113_format_info(fse->code)->code != fse->code)
+		return -EINVAL;
+
+	if (fse->index == 0) {
+		fse->min_width = 640;
+		fse->max_width = 640;
+		fse->min_height = 480;
+		fse->max_height = 480;
+	} else {
+		fse->min_width = 1280;
+		fse->max_width = 1280;
+		fse->min_height = 1024;
+		fse->max_height = 1024;
+	}
+
+	return 0;
+}
+
+static int mt9m113_ifp_enum_frame_interval(struct v4l2_subdev *sd,
+					   struct v4l2_subdev_state *state,
+					   struct v4l2_subdev_frame_interval_enum *fie)
+{
+	/* Only source pad (pad 1) supports frame interval enumeration */
+	if (fie->pad != 1)
+		return -EINVAL;
+
+	/* One interval per resolution */
+	if (fie->index > 0)
+		return -EINVAL;
+
+	/* Context A: 640x480 @ 30fps, Context B: 1280x1024 @ 15fps */
+	if (fie->width == 640 && fie->height == 480) {
+		fie->interval.numerator = 1;
+		fie->interval.denominator = 30;
+	} else if (fie->width == 1280 && fie->height == 1024) {
+		fie->interval.numerator = 1;
+		fie->interval.denominator = 15;
+	} else {
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int mt9m113_ifp_g_frame_interval(struct v4l2_subdev *sd,
+					struct v4l2_subdev_state *state,
+					struct v4l2_subdev_frame_interval *fi)
+{
+	struct v4l2_mbus_framefmt *format;
+
+	/* Frame intervals are only defined on the source pad. */
+	if (fi->pad != 1)
+		return -EINVAL;
+
+	format = v4l2_subdev_state_get_format(state, 1);
+
+	/* Return interval based on current resolution */
+	if (format->width <= 640 && format->height <= 480) {
+		/* Context A: 30fps */
+		fi->interval.numerator = 1;
+		fi->interval.denominator = 30;
+	} else {
+		/* Context B: 15fps */
+		fi->interval.numerator = 1;
+		fi->interval.denominator = 15;
+	}
+
+	return 0;
+}
+
+static int mt9m113_ifp_set_fmt(struct v4l2_subdev *sd,
+			       struct v4l2_subdev_state *state,
+			       struct v4l2_subdev_format *fmt)
+{
+	struct mt9m113 *sensor = ifp_to_mt9m113(sd);
+	struct v4l2_mbus_framefmt *format;
+	struct v4l2_rect *compose;
+	const struct mt9m113_format_info *info;
+
+	/*
+	 * Reject ACTIVE geometry/format changes while the pipeline is live.
+	 * The sensor is only programmed in mt9m113_stream_on(); silently
+	 * updating the active state would let userspace see one format while
+	 * the wire still carries the previous one. TRY queries are read-only
+	 * scratchpad probes and are always safe.
+	 */
+	if (sensor->streaming && fmt->which == V4L2_SUBDEV_FORMAT_ACTIVE)
+		return -EBUSY;
+
+	/* Sink pad format is fixed */
+	if (fmt->pad == 0) {
+		format = v4l2_subdev_state_get_format(state, 0);
+		fmt->format = *format;
+		return 0;
+	}
+
+	/* Source pad */
+	info = mt9m113_format_info(fmt->format.code);
+
+	/* Clamp to supported sizes (Context A or Context B) */
+	if (fmt->format.width <= 640) {
+		fmt->format.width = 640;
+		fmt->format.height = 480;
+	} else {
+		fmt->format.width = 1280;
+		fmt->format.height = 1024;
+	}
+
+	format = v4l2_subdev_state_get_format(state, 1);
+	format->width = fmt->format.width;
+	format->height = fmt->format.height;
+	format->code = info->code;
+	format->field = V4L2_FIELD_NONE;
+	format->colorspace = V4L2_COLORSPACE_SRGB;
+
+	compose = v4l2_subdev_state_get_compose(state, 0);
+	compose->width = format->width;
+	compose->height = format->height;
+
+	fmt->format = *format;
+	return 0;
+}
+
+static int mt9m113_ifp_get_selection(struct v4l2_subdev *sd,
+				     struct v4l2_subdev_state *state,
+				     struct v4l2_subdev_selection *sel)
+{
+	if (sel->pad != 0)
+		return -EINVAL;
+
+	switch (sel->target) {
+	case V4L2_SEL_TGT_CROP:
+		sel->r = *v4l2_subdev_state_get_crop(state, 0);
+		return 0;
+
+	case V4L2_SEL_TGT_COMPOSE:
+		sel->r = *v4l2_subdev_state_get_compose(state, 0);
+		return 0;
+
+	case V4L2_SEL_TGT_CROP_DEFAULT:
+	case V4L2_SEL_TGT_CROP_BOUNDS:
+		sel->r.left = 0;
+		sel->r.top = 0;
+		sel->r.width = MT9M113_PIXEL_ARRAY_WIDTH;
+		sel->r.height = MT9M113_PIXEL_ARRAY_HEIGHT;
+		return 0;
+
+	case V4L2_SEL_TGT_COMPOSE_DEFAULT:
+	case V4L2_SEL_TGT_COMPOSE_BOUNDS:
+		sel->r.left = 0;
+		sel->r.top = 0;
+		sel->r.width = MT9M113_PIXEL_ARRAY_WIDTH;
+		sel->r.height = MT9M113_PIXEL_ARRAY_HEIGHT;
+		return 0;
+
+	default:
+		return -EINVAL;
+	}
+}
+
+/* IFP registered callback - registers PA and creates link */
+static void mt9m113_ifp_unregistered(struct v4l2_subdev *sd)
+{
+	struct mt9m113 *sensor = ifp_to_mt9m113(sd);
+
+	v4l2_device_unregister_subdev(&sensor->pa.sd);
+}
+
+static int mt9m113_ifp_registered(struct v4l2_subdev *sd)
+{
+	struct mt9m113 *sensor = ifp_to_mt9m113(sd);
+	int ret;
+
+	ret = v4l2_device_register_subdev(sd->v4l2_dev, &sensor->pa.sd);
+	if (ret < 0) {
+		dev_err(&sensor->client->dev,
+			"Failed to register pixel array subdev\n");
+		return ret;
+	}
+
+	ret = media_create_pad_link(&sensor->pa.sd.entity, 0,
+				    &sensor->ifp.sd.entity, 0,
+				    MEDIA_LNK_FL_ENABLED |
+				    MEDIA_LNK_FL_IMMUTABLE);
+	if (ret < 0) {
+		dev_err(&sensor->client->dev,
+			"Failed to link pixel array to ifp\n");
+		v4l2_device_unregister_subdev(&sensor->pa.sd);
+		return ret;
+	}
+
+	return 0;
+}
+
+static const struct v4l2_subdev_video_ops mt9m113_ifp_video_ops = {
+	.s_stream = mt9m113_ifp_s_stream,
+};
+
+static const struct v4l2_subdev_pad_ops mt9m113_ifp_pad_ops = {
+	.enum_mbus_code = mt9m113_ifp_enum_mbus_code,
+	.enum_frame_size = mt9m113_ifp_enum_frame_size,
+	.enum_frame_interval = mt9m113_ifp_enum_frame_interval,
+	.get_fmt = v4l2_subdev_get_fmt,
+	.set_fmt = mt9m113_ifp_set_fmt,
+	.get_frame_interval = mt9m113_ifp_g_frame_interval,
+	.get_selection = mt9m113_ifp_get_selection,
+};
+
+static const struct v4l2_subdev_ops mt9m113_ifp_ops = {
+	.video = &mt9m113_ifp_video_ops,
+	.pad = &mt9m113_ifp_pad_ops,
+};
+
+/* See mt9m113_pa_release() above for why per-subdev cleanup is deferred. */
+static void mt9m113_ifp_release(struct v4l2_subdev *sd)
+{
+	struct mt9m113 *sensor = ifp_to_mt9m113(sd);
+
+	v4l2_subdev_cleanup(&sensor->ifp.sd);
+	v4l2_ctrl_handler_free(&sensor->ifp.hdl);
+	media_entity_cleanup(&sensor->ifp.sd.entity);
+	mt9m113_release_sensor(sensor);
+}
+
+static const struct v4l2_subdev_internal_ops mt9m113_ifp_internal_ops = {
+	.init_state = mt9m113_ifp_init_state,
+	.registered = mt9m113_ifp_registered,
+	.unregistered = mt9m113_ifp_unregistered,
+	.release = mt9m113_ifp_release,
+};
+
+/* -----------------------------------------------------------------------------
+ * Controls
+ */
+
+/*
+ * Menu items map directly to MT9M113_TEST_MODE_* values in datasheet order
+ * (MT9M113 datasheet R0x0066 "Test Pattern Generator"). The driver writes
+ * the menu index unchanged to MT9M113_MODE_TEST_MODE; no value translation
+ * is needed.
+ */
+static const char * const mt9m113_test_pattern_menu[] = {
+	"Disabled",
+	"Solid White",
+	"Grey Ramp",
+	"Color Bar Ramp",
+	"Solid White (Color Bars)",
+	"Noise",
+};
+
+static int mt9m113_s_ctrl(struct v4l2_ctrl *ctrl)
+{
+	struct mt9m113 *sensor = container_of(ctrl->handler,
+					       struct mt9m113, ifp.hdl);
+	int pm_ret, ret = 0;
+
+	/*
+	 * pm_runtime_get_if_in_use() returns >0 on success (HW active, kref
+	 * incremented), 0 if the device is suspended, and <0 on any other
+	 * condition (CONFIG_PM=n, or pm_runtime_disable() has been called
+	 * during unbind). Treat any non-positive return as "skip" so that
+	 * a control set issued after mt9m113_remove() called
+	 * pm_runtime_disable() does NOT proceed to dereference the
+	 * possibly-freed regmap. Requiring PM rather than gracefully
+	 * faking it for CONFIG_PM=n is acceptable -- the Kconfig already
+	 * implies regulator + clk which themselves depend on PM.
+	 */
+	pm_ret = pm_runtime_get_if_in_use(&sensor->client->dev);
+	if (pm_ret <= 0)
+		return 0;
+
+	switch (ctrl->id) {
+	case V4L2_CID_HFLIP: {
+		u64 mode_a, mode_b;
+
+		ret = mt9m113_read_mcu_var(sensor, MT9M113_SENSOR_READ_MODE_A,
+					   &mode_a);
+		if (ret)
+			break;
+		ret = mt9m113_read_mcu_var(sensor, MT9M113_SENSOR_READ_MODE_B,
+					   &mode_b);
+		if (ret)
+			break;
+
+		if (ctrl->val) {
+			mode_a |= MT9M113_SENSOR_READ_MODE_HMIRROR;
+			mode_b |= MT9M113_SENSOR_READ_MODE_HMIRROR;
+		} else {
+			mode_a &= ~MT9M113_SENSOR_READ_MODE_HMIRROR;
+			mode_b &= ~MT9M113_SENSOR_READ_MODE_HMIRROR;
+		}
+
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_SENSOR_READ_MODE_A,
+					    mode_a);
+		if (!ret)
+			ret = mt9m113_write_mcu_var(sensor,
+						    MT9M113_SENSOR_READ_MODE_B,
+						    mode_b);
+		/*
+		 * Only refresh if streaming and the MCU is not held in boot
+		 * mode for the test pattern - calling REFRESH while the MCU
+		 * is halted (test_pattern_active) blocks for two 500 ms
+		 * SEQ_CMD timeouts and never applies the new control.
+		 */
+		if (!ret && sensor->streaming && !sensor->test_pattern_active)
+			mt9m113_refresh(sensor);
+		break;
+	}
+
+	case V4L2_CID_VFLIP: {
+		u64 mode_a, mode_b;
+
+		ret = mt9m113_read_mcu_var(sensor, MT9M113_SENSOR_READ_MODE_A,
+					   &mode_a);
+		if (ret)
+			break;
+		ret = mt9m113_read_mcu_var(sensor, MT9M113_SENSOR_READ_MODE_B,
+					   &mode_b);
+		if (ret)
+			break;
+
+		if (ctrl->val) {
+			mode_a |= MT9M113_SENSOR_READ_MODE_VMIRROR;
+			mode_b |= MT9M113_SENSOR_READ_MODE_VMIRROR;
+		} else {
+			mode_a &= ~MT9M113_SENSOR_READ_MODE_VMIRROR;
+			mode_b &= ~MT9M113_SENSOR_READ_MODE_VMIRROR;
+		}
+
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_SENSOR_READ_MODE_A,
+					    mode_a);
+		if (!ret)
+			ret = mt9m113_write_mcu_var(sensor,
+						    MT9M113_SENSOR_READ_MODE_B,
+						    mode_b);
+		/*
+		 * Only refresh if streaming and the MCU is not held in boot
+		 * mode for the test pattern - calling REFRESH while the MCU
+		 * is halted (test_pattern_active) blocks for two 500 ms
+		 * SEQ_CMD timeouts and never applies the new control.
+		 */
+		if (!ret && sensor->streaming && !sensor->test_pattern_active)
+			mt9m113_refresh(sensor);
+		break;
+	}
+
+	case V4L2_CID_COLORFX: {
+		u16 effect;
+
+		switch (ctrl->val) {
+		case V4L2_COLORFX_NONE:
+			effect = MT9M113_SPEC_EFFECTS_NONE;
+			break;
+		case V4L2_COLORFX_BW:
+			effect = MT9M113_SPEC_EFFECTS_MONOCHROME;
+			break;
+		case V4L2_COLORFX_SEPIA:
+			effect = MT9M113_SPEC_EFFECTS_SEPIA;
+			break;
+		case V4L2_COLORFX_NEGATIVE:
+			effect = MT9M113_SPEC_EFFECTS_NEGATIVE;
+			break;
+		case V4L2_COLORFX_SOLARIZATION:
+			effect = MT9M113_SPEC_EFFECTS_SOLARIZE;
+			break;
+		default:
+			ret = -EINVAL;
+			break;
+		}
+
+		if (!ret) {
+			effect |= (MT9M113_SPEC_EFFECTS_DEFAULT &
+				   ~MT9M113_SPEC_EFFECTS_MASK);
+			ret = mt9m113_write_mcu_var(sensor,
+						    MT9M113_MODE_SPEC_EFFECTS_A,
+						    effect);
+			if (!ret)
+				ret = mt9m113_write_mcu_var(sensor,
+							    MT9M113_MODE_SPEC_EFFECTS_B,
+							    effect);
+
+			/* See HFLIP/VFLIP: skip refresh while test_pattern halts MCU. */
+			if (!ret && sensor->streaming && !sensor->test_pattern_active)
+				mt9m113_refresh(sensor);
+		}
+		break;
+	}
+
+	case V4L2_CID_POWER_LINE_FREQUENCY: {
+		u8 fd_mode;
+
+		switch (ctrl->val) {
+		case V4L2_CID_POWER_LINE_FREQUENCY_DISABLED:
+			fd_mode = MT9M113_FD_MODE_DISABLED;
+			break;
+		case V4L2_CID_POWER_LINE_FREQUENCY_50HZ:
+			fd_mode = MT9M113_FD_MODE_50HZ;
+			break;
+		case V4L2_CID_POWER_LINE_FREQUENCY_60HZ:
+			fd_mode = MT9M113_FD_MODE_60HZ;
+			break;
+		case V4L2_CID_POWER_LINE_FREQUENCY_AUTO:
+			fd_mode = MT9M113_FD_MODE_AUTO;
+			break;
+		default:
+			ret = -EINVAL;
+			break;
+		}
+
+		if (!ret)
+			ret = mt9m113_write_mcu_var(sensor, MT9M113_FD_MODE,
+						    fd_mode);
+		break;
+	}
+
+	case V4L2_CID_SATURATION:
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AWB_SATURATION,
+					    ctrl->val);
+		/*
+		 * Only refresh if streaming and the MCU is not held in boot
+		 * mode for the test pattern - calling REFRESH while the MCU
+		 * is halted (test_pattern_active) blocks for two 500 ms
+		 * SEQ_CMD timeouts and never applies the new control.
+		 */
+		if (!ret && sensor->streaming && !sensor->test_pattern_active)
+			mt9m113_refresh(sensor);
+		break;
+
+	case V4L2_CID_EXPOSURE_AUTO:
+		/* Enable/disable internal AE algorithm via AE_GATE */
+		if (ctrl->val == V4L2_EXPOSURE_AUTO)
+			ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_GATE,
+						    MT9M113_AE_GATE_ENABLE);
+		else
+			ret = mt9m113_write_mcu_var(sensor, MT9M113_AE_GATE,
+						    MT9M113_AE_GATE_DISABLE);
+		break;
+
+	case V4L2_CID_EXPOSURE:
+		/* Write coarse integration time to both contexts */
+		ret = cci_write(sensor->regmap, MT9M113_COARSE_IT_TIME_A,
+				ctrl->val, NULL);
+		if (!ret)
+			ret = cci_write(sensor->regmap, MT9M113_COARSE_IT_TIME_B,
+					ctrl->val, NULL);
+		break;
+
+	case V4L2_CID_ANALOGUE_GAIN:
+		/* Write analog gain register */
+		ret = cci_write(sensor->regmap, MT9M113_ANALOG_GAIN,
+				ctrl->val, NULL);
+		break;
+
+	case V4L2_CID_AUTO_WHITE_BALANCE:
+		/* Enable/disable internal AWB via awb_mode */
+		ret = mt9m113_write_mcu_var(sensor, MT9M113_AWB_MODE,
+					    ctrl->val ? 0x02 : 0x00);
+		break;
+
+	case V4L2_CID_TEST_PATTERN:
+		if (ctrl->val == 0) {
+			/*
+			 * Disable test pattern. Only restart MCU if test
+			 * pattern was previously active. On first call
+			 * (ctrl_handler_setup default), the MCU is already
+			 * in normal mode - restarting it here would disrupt
+			 * the MCU state and cause REFRESH timeouts.
+			 *
+			 * Keep test_pattern_active set until both writes
+			 * succeed: a half-applied disable leaves the MCU
+			 * halted, and a subsequent CID=0 must be allowed to
+			 * retry the full sequence.
+			 */
+			if (sensor->test_pattern_active) {
+				dev_dbg(&sensor->client->dev,
+					 "MT9M113: Disabling test pattern, restarting MCU\n");
+				/*
+				 * Reverse of mt9m113_apply_test_pattern(): release
+				 * the MCU from halt, write MODE_TEST_MODE=0, and
+				 * REFRESH if streaming so the sequencer reverts to
+				 * the normal sensor stream and frames resume.
+				 */
+				cci_write(sensor->regmap, MT9M113_MCU_BOOT_MODE,
+					  0x0000, &ret);
+				if (!ret) {
+					usleep_range(10000, 15000);
+					ret = mt9m113_write_mcu_var(sensor,
+								    MT9M113_MODE_TEST_MODE,
+								    MT9M113_TEST_MODE_DISABLED);
+				}
+				if (!ret && sensor->streaming)
+					ret = mt9m113_refresh(sensor);
+				if (!ret) {
+					sensor->test_pattern_active = false;
+					sensor->test_pattern_value = 0;
+				}
+			}
+		} else {
+			/*
+			 * Enable test pattern mode.
+			 *
+			 * Only commit the MCU vars + halt here while we're
+			 * already streaming. Otherwise the MCU is asleep
+			 * (autosuspend) or not yet running the sequencer,
+			 * and either the writes would race a power_on or
+			 * the subsequent normal stream init would overwrite
+			 * MODE_SELECT. Cache the requested pattern in
+			 * test_pattern_value so mt9m113_stream_on() can
+			 * replay the apply at the end of its normal-init
+			 * sequence without doing a v4l2_ctrl_find() lookup
+			 * (which would self-deadlock on the ctrl-handler
+			 * mutex that the V4L2 core holds across s_stream).
+			 */
+			dev_dbg(&sensor->client->dev,
+				 "MT9M113: Enabling test pattern %d\n", ctrl->val);
+			/*
+			 * Only flip the bookkeeping flags after the MCU
+			 * writes actually succeed. If apply_test_pattern()
+			 * fails mid-sequence the chip is still in its normal
+			 * streaming mode, and leaving test_pattern_active
+			 * set would make every subsequent control (HFLIP,
+			 * VFLIP, COLORFX, SATURATION) skip its REFRESH
+			 * forever because they think the MCU is halted.
+			 *
+			 * When not streaming, no MCU write happens here -
+			 * stream_on() replays apply_test_pattern() once the
+			 * pipeline is up. In that case there is nothing to
+			 * fail, so caching the requested value is safe.
+			 */
+			if (sensor->streaming) {
+				ret = mt9m113_apply_test_pattern(sensor,
+								 ctrl->val);
+				if (ret)
+					break;
+			}
+			sensor->test_pattern_active = true;
+			sensor->test_pattern_value = ctrl->val;
+		}
+		break;
+
+	default:
+		ret = -EINVAL;
+		break;
+	}
+
+	if (pm_ret > 0)
+		pm_runtime_put_autosuspend(&sensor->client->dev);
+	return ret;
+}
+
+static const struct v4l2_ctrl_ops mt9m113_ctrl_ops = {
+	.s_ctrl = mt9m113_s_ctrl,
+};
+
+/* -----------------------------------------------------------------------------
+ * Power Management
+ */
+
+static int mt9m113_power_on(struct mt9m113 *sensor)
+{
+	struct device *dev = &sensor->client->dev;
+	int ret;
+
+	ret = regulator_bulk_enable(ARRAY_SIZE(sensor->supplies),
+				    sensor->supplies);
+	if (ret < 0)
+		return ret;
+
+	/*
+	 * Aptina/onsemi SOC sensors require EXTCLK to be running before
+	 * STANDBY (powerdown) is released; otherwise the internal
+	 * clock-domain state machines come out of reset non-deterministically
+	 * and the MCU can wedge on SEQ_CMD.  Enable the clock first, let the
+	 * supplies and clock settle, then deassert powerdown.
+	 */
+	ret = clk_prepare_enable(sensor->clk);
+	if (ret < 0)
+		goto error_regulator;
+
+	usleep_range(20000, 25000);
+
+	if (sensor->powerdown)
+		gpiod_set_value_cansleep(sensor->powerdown, 0);
+
+	msleep(20);
+
+	if (sensor->reset) {
+		gpiod_set_value_cansleep(sensor->reset, 1);
+		usleep_range(1000, 2000);
+		gpiod_set_value_cansleep(sensor->reset, 0);
+		usleep_range(44500, 50000);
+	} else if (sensor->powerdown) {
+		usleep_range(44500, 50000);
+	}
+
+	/* MT9M113 MCU boot sequence */
+	{
+		u64 clocks_val = 0;
+
+		cci_read(sensor->regmap, MT9M113_CLOCKS_CONTROL, &clocks_val, NULL);
+		if (clocks_val != 0) {
+			u64 seq_cmd = 0;
+			int read_ret;
+
+			/*
+			 * Sensor already has clocks running (warm reboot or
+			 * resume). Check if MCU is responsive by reading
+			 * SEQ_CMD. If stuck (non-zero) or if the read itself
+			 * failed (I2C error), force the soft-reset path instead
+			 * of silently returning success on an uninitialised
+			 * sensor.
+			 */
+			read_ret = mt9m113_read_mcu_var(sensor, MT9M113_SEQ_CMD,
+							&seq_cmd);
+			if (read_ret == 0 && seq_cmd == 0) {
+				dev_dbg(dev, "MT9M113 already initialized, MCU OK\n");
+				msleep(50);
+				return 0;
+			}
+			if (read_ret < 0)
+				dev_warn(dev,
+					 "MT9M113: SEQ_CMD read failed (%d), forcing soft reset\n",
+					 read_ret);
+			else
+				dev_warn(dev,
+					 "MT9M113: MCU stuck (SEQ_CMD=0x%llx), forcing soft reset\n",
+					 seq_cmd);
+		}
+
+		/* Soft reset */
+		cci_write(sensor->regmap, MT9M113_RESET_AND_MISC_CONTROL,
+			  MT9M113_RESET_SOC, &ret);
+		cci_write(sensor->regmap, MT9M113_RESET_AND_MISC_CONTROL,
+			  0, &ret);
+		if (ret < 0)
+			goto error_clock;
+		msleep(200);
+
+		/* Boot MCU */
+		cci_write(sensor->regmap, MT9M113_MCU_BOOT_MODE, 0x0001, &ret);
+		usleep_range(1000, 2000);
+		cci_write(sensor->regmap, MT9M113_MCU_BOOT_MODE, 0x0000, &ret);
+		if (ret < 0)
+			goto error_clock;
+		msleep(200);
+
+		/* Configure PLL */
+		cci_write(sensor->regmap, MT9M113_CLOCKS_CONTROL, 0x00FF, &ret);
+		cci_write(sensor->regmap, MT9M113_STANDBY_CONTROL,
+			  MT9M113_STANDBY_CONTROL_ACTIVE, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_CONTROL, 0x2145, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_CONTROL, 0x2145, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_CONTROL, 0x2145, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_DIVIDERS, 0x0114, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_P_DIVIDERS, 0x00F1, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_CONTROL, 0x2545, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_CONTROL, 0x2547, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_CONTROL, 0x3447, &ret);
+		if (ret < 0)
+			goto error_clock;
+		msleep(20);
+		cci_write(sensor->regmap, MT9M113_PLL_CONTROL, 0x3047, &ret);
+		cci_write(sensor->regmap, MT9M113_PLL_CONTROL, 0x3046, &ret);
+		cci_write(sensor->regmap, MT9M113_RESET_AND_MISC_CONTROL, 0x0218, &ret);
+		/* Keep the MCU active (do not enter standby 0x002A) after power-on. */
+		cci_write(sensor->regmap, MT9M113_STANDBY_CONTROL,
+			  MT9M113_STANDBY_CONTROL_ACTIVE, &ret);
+		if (ret < 0)
+			goto error_clock;
+		msleep(50);
+
+		/* Configure OFIFO */
+		cci_write(sensor->regmap, MT9M113_OFIFO_CONTROL_STATUS, 0x0003, &ret);
+		if (ret < 0)
+			goto error_clock;
+	}
+
+	return 0;
+
+error_clock:
+	clk_disable_unprepare(sensor->clk);
+error_regulator:
+	if (sensor->powerdown)
+		gpiod_set_value_cansleep(sensor->powerdown, 1);
+	else if (sensor->reset)
+		gpiod_set_value_cansleep(sensor->reset, 1);
+	regulator_bulk_disable(ARRAY_SIZE(sensor->supplies),
+			       sensor->supplies);
+	return ret;
+}
+
+static void mt9m113_power_off(struct mt9m113 *sensor)
+{
+	/*
+	 * Symmetric with mt9m113_power_on(): hold the chip in reset, then
+	 * gate the clock and drop the supplies so the enable/disable refcount
+	 * stays balanced across runtime-PM cycles and the MCU is fully reset.
+	 *
+	 * Prefer the powerdown GPIO when available; fall back to driving
+	 * RESET_BAR active so it is not left floating after VDD drops on
+	 * boards that only wire reset-gpios.  Mirror the settling gaps from
+	 * power_on so the MCU sees a clean down-then-up sequence across
+	 * runtime-PM cycles.
+	 */
+	if (sensor->powerdown)
+		gpiod_set_value_cansleep(sensor->powerdown, 1);
+	else if (sensor->reset)
+		gpiod_set_value_cansleep(sensor->reset, 1);
+	usleep_range(1000, 2000);
+	clk_disable_unprepare(sensor->clk);
+	usleep_range(1000, 2000);
+	regulator_bulk_disable(ARRAY_SIZE(sensor->supplies),
+			       sensor->supplies);
+}
+
+static int mt9m113_runtime_resume(struct device *dev)
+{
+	struct v4l2_subdev *sd = dev_get_drvdata(dev);
+	struct mt9m113 *sensor = ifp_to_mt9m113(sd);
+	int ret;
+
+	/*
+	 * Always power-cycle and re-initialise on resume, including when a
+	 * powerdown GPIO is present. The MT9M113 MCU must be hardware-reset
+	 * (powerdown + clock) and re-initialised on each session, otherwise a
+	 * wedged MCU (SEQ_CMD/0xA103 stuck) is never recovered and only a
+	 * physical power cycle helps - matching the legacy vendor kernel per-open behaviour.
+	 */
+	ret = mt9m113_power_on(sensor);
+	if (ret)
+		return ret;
+
+	ret = mt9m113_sensor_init(sensor);
+	if (ret) {
+		/*
+		 * sensor_init() can fail (-ETIMEDOUT on an MCU lockup, I2C
+		 * errors); on that path the PM core reverts runtime_status to
+		 * SUSPENDED without touching driver-side state, so the
+		 * clk/regulator refcounts taken in power_on() would otherwise
+		 * leak and the next resume would double-enable them.
+		 */
+		mt9m113_power_off(sensor);
+		return ret;
+	}
+
+	/* Resume succeeded; clear any stale chip_off bookkeeping from a
+	 * prior failed start_streaming retry loop. */
+	sensor->chip_off = false;
+	return 0;
+}
+
+static int mt9m113_runtime_suspend(struct device *dev)
+{
+	struct v4l2_subdev *sd = dev_get_drvdata(dev);
+	struct mt9m113 *sensor = ifp_to_mt9m113(sd);
+
+	/*
+	 * If start_streaming's retry loop already powered the chip down
+	 * (clk + regulator refcounts already balanced to zero), skip our
+	 * own power_off to avoid underflowing them. Consume the flag.
+	 */
+	if (sensor->chip_off) {
+		sensor->chip_off = false;
+		return 0;
+	}
+
+	/* Power down (powerdown GPIO + clock off) so the MCU is reset next resume. */
+	mt9m113_power_off(sensor);
+	return 0;
+}
+
+static DEFINE_RUNTIME_DEV_PM_OPS(mt9m113_pm_ops,
+				 mt9m113_runtime_suspend,
+				 mt9m113_runtime_resume,
+				 NULL);
+
+/* -----------------------------------------------------------------------------
+ * Probe & Remove
+ */
+
+static int mt9m113_identify(struct mt9m113 *sensor)
+{
+	u64 value;
+	int ret;
+
+	ret = cci_read(sensor->regmap, MT9M113_CHIP_ID, &value, NULL);
+	if (ret) {
+		dev_err(&sensor->client->dev, "Failed to read chip ID\n");
+		return -ENXIO;
+	}
+
+	if (value != MT9M113_CHIP_ID_VALUE) {
+		dev_err(&sensor->client->dev,
+			"Invalid chip ID 0x%04llx (expected 0x%04x)\n",
+			value, MT9M113_CHIP_ID_VALUE);
+		return -ENXIO;
+	}
+
+	dev_dbg(&sensor->client->dev, "MT9M113 detected (ID 0x%04llx)\n",
+		 value);
+	return 0;
+}
+
+static int mt9m113_parse_dt(struct mt9m113 *sensor)
+{
+	struct fwnode_handle *fwnode = dev_fwnode(&sensor->client->dev);
+	struct fwnode_handle *ep;
+	int ret;
+
+	ep = fwnode_graph_get_next_endpoint(fwnode, NULL);
+	if (!ep)
+		return -EINVAL;
+
+	sensor->bus_cfg.bus_type = V4L2_MBUS_UNKNOWN;
+	ret = v4l2_fwnode_endpoint_alloc_parse(ep, &sensor->bus_cfg);
+	fwnode_handle_put(ep);
+	if (ret < 0)
+		return ret;
+
+	if (sensor->bus_cfg.bus_type != V4L2_MBUS_CSI2_DPHY) {
+		dev_err(&sensor->client->dev, "Unsupported bus type %d\n",
+			sensor->bus_cfg.bus_type);
+		v4l2_fwnode_endpoint_free(&sensor->bus_cfg);
+		return -EINVAL;
+	}
+
+	if (sensor->bus_cfg.nr_of_link_frequencies < 1) {
+		dev_err(&sensor->client->dev, "no link-frequencies specified\n");
+		v4l2_fwnode_endpoint_free(&sensor->bus_cfg);
+		return -EINVAL;
+	}
+
+	/*
+	 * The MT9M113 has a single-lane MIPI CSI-2 D-PHY interface, so the
+	 * binding no longer advertises data-lanes (a maxItems=1 array adds no
+	 * information). v4l2_fwnode_endpoint_parse() therefore leaves
+	 * num_data_lanes at 0; force it to 1 so the pixrate calculation
+	 * below has a non-zero multiplier and receivers like camss size the
+	 * CSI-2 link budget correctly.
+	 */
+	if (sensor->bus_cfg.bus.mipi_csi2.num_data_lanes == 0)
+		sensor->bus_cfg.bus.mipi_csi2.num_data_lanes = 1;
+
+	if (sensor->bus_cfg.bus.mipi_csi2.num_data_lanes != 1) {
+		dev_err(&sensor->client->dev,
+			"unsupported data-lanes count %u (must be 1)\n",
+			sensor->bus_cfg.bus.mipi_csi2.num_data_lanes);
+		v4l2_fwnode_endpoint_free(&sensor->bus_cfg);
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int mt9m113_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct mt9m113 *sensor;
+	int ret;
+
+	sensor = kzalloc(sizeof(*sensor), GFP_KERNEL);
+	if (!sensor)
+		return -ENOMEM;
+
+	sensor->client = client;
+	/*
+	 * Initialise to 2 so kfree(sensor) only runs after both the PA and IFP
+	 * release callbacks have fired (i.e. all open /dev/v4l-subdev* fds have
+	 * been closed). See mt9m113_pa_release() for the full rationale.
+	 */
+	atomic_set(&sensor->release_count, 2);
+
+	sensor->regmap = devm_cci_regmap_init_i2c(client, 16);
+	if (IS_ERR(sensor->regmap)) {
+		ret = PTR_ERR(sensor->regmap);
+		goto error_kfree;
+	}
+
+	ret = mt9m113_parse_dt(sensor);
+	if (ret < 0)
+		goto error_kfree;
+
+	sensor->clk = devm_clk_get(dev, NULL);
+	if (IS_ERR(sensor->clk)) {
+		ret = PTR_ERR(sensor->clk);
+		goto error_ep_free;
+	}
+
+	sensor->reset = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_LOW);
+	if (IS_ERR(sensor->reset)) {
+		ret = PTR_ERR(sensor->reset);
+		goto error_ep_free;
+	}
+
+	sensor->powerdown = devm_gpiod_get_optional(dev, "powerdown",
+						    GPIOD_OUT_HIGH);
+	if (IS_ERR(sensor->powerdown)) {
+		ret = PTR_ERR(sensor->powerdown);
+		goto error_ep_free;
+	}
+
+	sensor->supplies[0].supply = "vddio";
+	sensor->supplies[1].supply = "vdd";
+	sensor->supplies[2].supply = "vaa";
+
+	ret = devm_regulator_bulk_get(dev, ARRAY_SIZE(sensor->supplies),
+				      sensor->supplies);
+	if (ret < 0)
+		goto error_ep_free;
+
+	/* Power on and identify */
+	ret = mt9m113_power_on(sensor);
+	if (ret < 0)
+		goto error_ep_free;
+
+	ret = mt9m113_identify(sensor);
+	if (ret < 0)
+		goto error_power_off;
+
+	ret = mt9m113_sensor_init(sensor);
+	if (ret < 0)
+		goto error_power_off;
+
+	/*
+	 * The CSI-2 link frequency is mandatory (enforced by the binding and
+	 * mt9m113_parse_dt()). Derive the pixel rate from it: every supported
+	 * source format is 16 bits/pixel on the bus and the D-PHY clocks data
+	 * DDR, so pixel_rate = link_freq * 2 * lanes / bpp.
+	 */
+	sensor->link_freq = sensor->bus_cfg.link_frequencies[0];
+	sensor->pixrate = div_u64(2ULL * sensor->link_freq *
+				  sensor->bus_cfg.bus.mipi_csi2.num_data_lanes,
+				  16);
+
+	dev_dbg(dev, "MT9M113: link freq %lld Hz, pixel rate %u Hz\n",
+		sensor->link_freq, sensor->pixrate);
+
+	/* Initialize Pixel Array subdev */
+	v4l2_subdev_init(&sensor->pa.sd, &mt9m113_pa_ops);
+	sensor->pa.sd.internal_ops = &mt9m113_pa_internal_ops;
+	v4l2_i2c_subdev_set_name(&sensor->pa.sd, client, NULL, " pixel array");
+	sensor->pa.sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
+	sensor->pa.sd.owner = THIS_MODULE;
+	sensor->pa.sd.dev = dev;
+	v4l2_set_subdevdata(&sensor->pa.sd, client);
+	sensor->pa.sd.entity.function = MEDIA_ENT_F_CAM_SENSOR;
+	sensor->pa.pad.flags = MEDIA_PAD_FL_SOURCE;
+	ret = media_entity_pads_init(&sensor->pa.sd.entity, 1, &sensor->pa.pad);
+	if (ret < 0)
+		goto error_pa_entity;
+
+	/*
+	 * LINK_FREQ and PIXEL_RATE live on the pixel-array subdev because
+	 * receivers walk the media graph upstream until they reach the
+	 * MEDIA_ENT_F_CAM_SENSOR entity and read these controls from that
+	 * subdev's handler (see camss_find_sensor_pad() / v4l2_get_link_freq()
+	 * in drivers/media/platform/qcom/camss/camss.c). The IFP is a
+	 * MEDIA_ENT_F_PROC_VIDEO_ISP and the walk does not stop there, so
+	 * controls on the IFP alone are invisible to the receiver and the
+	 * pipeline fails at CSIPHY stream-on with -EINVAL.
+	 */
+	v4l2_ctrl_handler_init(&sensor->pa.hdl, 2);
+	{
+		struct v4l2_ctrl *link_freq_ctrl;
+
+		link_freq_ctrl = v4l2_ctrl_new_int_menu(&sensor->pa.hdl, NULL,
+				V4L2_CID_LINK_FREQ,
+				sensor->bus_cfg.nr_of_link_frequencies - 1, 0,
+				sensor->bus_cfg.link_frequencies);
+		if (link_freq_ctrl)
+			link_freq_ctrl->flags |= V4L2_CTRL_FLAG_READ_ONLY;
+	}
+	v4l2_ctrl_new_std(&sensor->pa.hdl, NULL, V4L2_CID_PIXEL_RATE,
+			  sensor->pixrate, sensor->pixrate, 1, sensor->pixrate);
+	if (sensor->pa.hdl.error) {
+		ret = sensor->pa.hdl.error;
+		goto error_pa_handler;
+	}
+	sensor->pa.sd.ctrl_handler = &sensor->pa.hdl;
+	sensor->pa.sd.state_lock = sensor->pa.hdl.lock;
+
+	ret = v4l2_subdev_init_finalize(&sensor->pa.sd);
+	if (ret < 0)
+		goto error_pa_handler;
+
+	/* Initialize IFP subdev */
+	v4l2_i2c_subdev_init(&sensor->ifp.sd, client, &mt9m113_ifp_ops);
+	v4l2_i2c_subdev_set_name(&sensor->ifp.sd, client, NULL, " ifp");
+	sensor->ifp.sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
+	sensor->ifp.sd.internal_ops = &mt9m113_ifp_internal_ops;
+	sensor->ifp.sd.entity.function = MEDIA_ENT_F_PROC_VIDEO_ISP;
+	sensor->ifp.pads[0].flags = MEDIA_PAD_FL_SINK;
+	sensor->ifp.pads[1].flags = MEDIA_PAD_FL_SOURCE;
+	ret = media_entity_pads_init(&sensor->ifp.sd.entity, 2, sensor->ifp.pads);
+	if (ret < 0)
+		goto error_ifp_entity;
+
+	/* Initialize controls on IFP */
+	v4l2_ctrl_handler_init(&sensor->ifp.hdl, 10);
+	v4l2_ctrl_new_std(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			  V4L2_CID_HFLIP, 0, 1, 1, 0);
+	v4l2_ctrl_new_std(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			  V4L2_CID_VFLIP, 0, 1, 1, 0);
+	/* COLORFX: mask out unsupported effects 4-12 (EMBOSS through SILHOUETTE) */
+	v4l2_ctrl_new_std_menu(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			       V4L2_CID_COLORFX,
+			       V4L2_COLORFX_SOLARIZATION, 0x1ff0,
+			       V4L2_COLORFX_NONE);
+	v4l2_ctrl_new_std_menu(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			       V4L2_CID_POWER_LINE_FREQUENCY,
+			       V4L2_CID_POWER_LINE_FREQUENCY_AUTO, 0,
+			       V4L2_CID_POWER_LINE_FREQUENCY_AUTO);
+	v4l2_ctrl_new_std(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			  V4L2_CID_SATURATION, 0, 255, 1, 128);
+	v4l2_ctrl_new_std_menu(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			       V4L2_CID_EXPOSURE_AUTO,
+			       V4L2_EXPOSURE_MANUAL, 0,
+			       V4L2_EXPOSURE_AUTO);
+	v4l2_ctrl_new_std(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			  V4L2_CID_EXPOSURE, 1, 1000, 1, 100);
+	v4l2_ctrl_new_std(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			  V4L2_CID_ANALOGUE_GAIN, 0, 127, 1, 32);
+	v4l2_ctrl_new_std(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+			  V4L2_CID_AUTO_WHITE_BALANCE, 0, 1, 1, 1);
+	v4l2_ctrl_new_std_menu_items(&sensor->ifp.hdl, &mt9m113_ctrl_ops,
+				     V4L2_CID_TEST_PATTERN,
+				     ARRAY_SIZE(mt9m113_test_pattern_menu) - 1,
+				     0, 0, mt9m113_test_pattern_menu);
+
+	if (sensor->ifp.hdl.error) {
+		ret = sensor->ifp.hdl.error;
+		goto error_ifp_handler;
+	}
+
+	sensor->ifp.sd.ctrl_handler = &sensor->ifp.hdl;
+	sensor->ifp.sd.state_lock = sensor->ifp.hdl.lock;
+
+	ret = v4l2_subdev_init_finalize(&sensor->ifp.sd);
+	if (ret < 0)
+		goto error_ifp_handler;
+
+	/*
+	 * Enable runtime PM.  Configure autosuspend before enabling the
+	 * runtime PM core so a put issued between enable and use_autosuspend
+	 * cannot race with autosuspend being unconfigured.  The 2 s autosuspend
+	 * delay amortises the unconditional power-cycle + full init-table
+	 * replay in runtime_resume over typical preview/snapshot sequences:
+	 * the cold init is load-bearing for MCU-wedge recovery so it cannot
+	 * be conditionalised on health, only amortised.
+	 */
+	pm_runtime_set_active(dev);
+	pm_runtime_get_noresume(dev);
+	pm_runtime_set_autosuspend_delay(dev, 2000);
+	pm_runtime_use_autosuspend(dev);
+	pm_runtime_enable(dev);
+
+	/* Register only the IFP - PA will be registered in ifp_registered callback */
+	ret = v4l2_async_register_subdev(&sensor->ifp.sd);
+	if (ret < 0)
+		goto error_pm;
+
+	pm_runtime_put_autosuspend(dev);
+
+	dev_dbg(dev, "MT9M113 driver with IFP sub-device initialized\n");
+	return 0;
+
+error_pm:
+	pm_runtime_disable(dev);
+	pm_runtime_set_suspended(dev);
+	pm_runtime_dont_use_autosuspend(dev);
+	pm_runtime_put_noidle(dev);
+error_ifp_handler:
+	v4l2_subdev_cleanup(&sensor->ifp.sd);
+	v4l2_ctrl_handler_free(&sensor->ifp.hdl);
+error_ifp_entity:
+	media_entity_cleanup(&sensor->ifp.sd.entity);
+	v4l2_subdev_cleanup(&sensor->pa.sd);
+error_pa_handler:
+	v4l2_ctrl_handler_free(&sensor->pa.hdl);
+error_pa_entity:
+	media_entity_cleanup(&sensor->pa.sd.entity);
+error_power_off:
+	mt9m113_power_off(sensor);
+error_ep_free:
+	v4l2_fwnode_endpoint_free(&sensor->bus_cfg);
+error_kfree:
+	kfree(sensor);
+	return ret;
+}
+
+static void mt9m113_remove(struct i2c_client *client)
+{
+	struct v4l2_subdev *sd = i2c_get_clientdata(client);
+	struct mt9m113 *sensor = ifp_to_mt9m113(sd);
+	struct device *dev = &client->dev;
+
+	/*
+	 * Tear down PM and power the chip off BEFORE calling
+	 * v4l2_async_unregister_subdev(). When no fds are open on the
+	 * /dev/v4l-subdev* nodes, async_unregister may synchronously
+	 * fire the per-subdev .release callbacks, the last of which
+	 * runs kfree(sensor); touching @sensor or @dev after that
+	 * would be a use-after-free.
+	 *
+	 * v4l2_fwnode_endpoint_free(&sensor->bus_cfg) is *deferred* to
+	 * mt9m113_release_sensor() (the release_count==0 finalizer)
+	 * because the link-frequencies array embedded in bus_cfg is
+	 * still referenced by the IFP control handler's qmenu_int
+	 * (set up via v4l2_ctrl_new_int_menu in probe). The ctrl
+	 * handler outlives remove() under the deferred-release model;
+	 * freeing the endpoint here would leave qmenu_int dangling
+	 * and turn a later VIDIOC_QUERYMENU into a UAF.
+	 */
+	pm_runtime_disable(dev);
+	pm_runtime_dont_use_autosuspend(dev);
+	if (!pm_runtime_status_suspended(dev))
+		mt9m113_power_off(sensor);
+	pm_runtime_set_suspended(dev);
+
+	v4l2_async_unregister_subdev(&sensor->ifp.sd);
+	/* @sensor may already be freed here; do not dereference it. */
+}
+
+static const struct of_device_id mt9m113_of_ids[] = {
+	{ .compatible = "aptina,mt9m113" },
+	{ /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(of, mt9m113_of_ids);
+
+static const struct i2c_device_id mt9m113_id[] = {
+	{ "mt9m113" },
+	{ /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(i2c, mt9m113_id);
+
+static struct i2c_driver mt9m113_driver = {
+	.driver = {
+		.name	= "mt9m113",
+		.pm	= pm_ptr(&mt9m113_pm_ops),
+		.of_match_table = mt9m113_of_ids,
+	},
+	.probe		= mt9m113_probe,
+	.remove		= mt9m113_remove,
+	.id_table	= mt9m113_id,
+};
+
+module_i2c_driver(mt9m113_driver);
+
+MODULE_AUTHOR("Herman van Hazendonk <github.com@herrie.org>");
+MODULE_DESCRIPTION("Aptina MT9M113 Sensor Driver");
+MODULE_LICENSE("GPL");

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 4+ messages in thread

* Re: [PATCH v6 2/2] media: i2c: add Aptina MT9M113 1.3 Mpx SoC sensor driver
  2026-06-06 19:01 ` [PATCH v6 2/2] media: i2c: add Aptina MT9M113 1.3 Mpx SoC sensor driver Herman van Hazendonk via B4 Relay
@ 2026-06-06 19:13   ` sashiko-bot
  0 siblings, 0 replies; 4+ messages in thread
From: sashiko-bot @ 2026-06-06 19:13 UTC (permalink / raw)
  To: Herman van Hazendonk via B4 Relay; +Cc: conor+dt, linux-media, robh, devicetree

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] The `mt9m113_start_streaming()` error path explicitly powers off the hardware but leaves the PM state as `RPM_ACTIVE`. Subsequent operations (like STREAMON retries, `s_ctrl`, or `remove`) trust the active PM state and perform illegal operations, causing clk/regulator refcount underflows and I2C timeouts.
- [High] Tying the allocation lifetime of the sensor structure to V4L2 subdev `.release` callbacks introduces a memory leak on unbound initialization and a Use-After-Free during bridge unbind.
- [High] Concurrency between `VIDIOC_S_CTRL` / `VIDIOC_STREAMON` ioctls and `mt9m113_remove()` causes Use-After-Free on devres-managed resources (e.g., `regmap`, `clk`).
--

commit f0fc2c67299ebd9a65a3dfa2e680f9a4ff64a2cb
Author: Herman van Hazendonk <github.com@herrie.org>

    media: i2c: add Aptina MT9M113 1.3 Mpx SoC sensor driver
    
    Add a V4L2 subdev driver for the Aptina MT9M113 1.3 Megapixel SoC
    image sensor (1280x1024 active pixel array) with on-chip ISP.

> diff --git a/drivers/media/i2c/mt9m113.c b/drivers/media/i2c/mt9m113.c
> --- a/drivers/media/i2c/mt9m113.c
> +++ b/drivers/media/i2c/mt9m113.c
> @@ -1876,14 +1876,8 @@ static int mt9m113_start_streaming(struct mt9m113 *sensor,
[ ... ]
>  	sensor->chip_off = true;
>  	pm_runtime_put_noidle(dev);
>  	return ret;
>  }

[Severity: High]
Does this leave the PM state as RPM_ACTIVE while the hardware is physically
powered off?

If the retry loop exhausts all attempts, the hardware is explicitly powered
down. However, because the PM state is left active, subsequent operations
like ioctls or remove() will trust the PM state and attempt hardware accesses
or call power down again.

Could this lead to clock and regulator refcount underflows in remove() or
I2C timeouts in s_ctrl() since they believe the chip is powered on?

[ ... ]

> @@ -2293,12 +2293,7 @@ static int mt9m113_ifp_registered(struct v4l2_subdev *sd)
>  {
>  	struct mt9m113 *sensor = ifp_to_mt9m113(sd);
>  	int ret;
>  
>  	ret = v4l2_device_register_subdev(sd->v4l2_dev, &sensor->pa.sd);

[ ... ]

> @@ -3246,26 +3246,12 @@ static void mt9m113_remove(struct i2c_client *client)
[ ... ]
>  	pm_runtime_disable(dev);
>  	pm_runtime_dont_use_autosuspend(dev);
>  	if (!pm_runtime_status_suspended(dev))
>  		mt9m113_power_off(sensor);

[Severity: High]
Can the sensor structure be freed before remove() is called or leaked
entirely?

The allocation lifetime of the sensor structure is tied to the V4L2 subdev
release callbacks, using an atomic release_count initialized to 2. However,
the pixel array subdev pa.sd is only registered in mt9m113_ifp_registered()
when a bridge attaches.

If the module is unloaded before a bridge binds, pa.sd is never registered,
its release callback never runs, and the sensor structure leaks.

Conversely, if a bridge unbinds, v4l2_device_unregister_subdev() invokes
the release callbacks for both subdevs, freeing the sensor structure. A
subsequent execution of remove() would then dereference the already freed
sensor when calling mt9m113_power_off() and v4l2_async_unregister_subdev().

Could this result in a use-after-free?

>  	pm_runtime_set_suspended(dev);
>  
>  	v4l2_async_unregister_subdev(&sensor->ifp.sd);

[Severity: High]
Does this remove process leave devres-managed resources vulnerable to a
use-after-free from concurrent ioctls?

By deferring the freeing of the sensor structure to the subdev release
callbacks, the control handlers can outlive remove() if an application holds
an open file descriptor.

Hardware resources like sensor->regmap and sensor->clk are managed by devres
and are automatically freed when remove() returns.

If an ioctl like VIDIOC_S_CTRL successfully calls pm_runtime_get_if_in_use()
just before remove() calls pm_runtime_disable(), will the control handler
thread attempt to use the regmap structure after devres has already destroyed
it?

>  	/* @sensor may already be freed here; do not dereference it. */
>  }

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260606-submit-media-mt9m113-v6-0-8f6d0f79f4d1@herrie.org?part=2

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-06-06 19:13 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-06 19:01 [PATCH v6 0/2] media: i2c: add Aptina MT9M113 image sensor driver Herman van Hazendonk via B4 Relay
2026-06-06 19:01 ` [PATCH v6 1/2] dt-bindings: media: i2c: add aptina,mt9m113 Herman van Hazendonk via B4 Relay
2026-06-06 19:01 ` [PATCH v6 2/2] media: i2c: add Aptina MT9M113 1.3 Mpx SoC sensor driver Herman van Hazendonk via B4 Relay
2026-06-06 19:13   ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox