Thread (4 messages) flat view 4 messages, 2 authors, 23d ago

Re: [PATCH 2/2] media: i2c: mira016: Add driver for Mira016

From: sashiko-bot@kernel.org
Date: 2026-09-03 12:32:27

Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [High] mira016_init_state() mutates global driver state when called for TRY states, corrupting active frame timings and creating a data race.
- [High] Out-of-bounds array access in analogue gain lookup table due to off-by-one maximum limit.
- [High] Control handler may access freed active state during driver removal due to incorrect cleanup ordering.
- [Medium] Error codes from I2C writes are silently overwritten and ignored in multiple configuration functions.
- [Medium] Active V4L2 state is modified before rejecting the operation if the device is currently streaming.
- [Medium] Severe precision loss in pixel rate calculation due to early integer division.
- [Medium] Hardware minimum vertical blanking is violated due to integer division rounding down.
--

commit 3cceafc720bbde80fa31e38aaa08374b0f450a04
Author: Jacopo Mondi [off-list ref]

media: i2c: mira016: Add driver for Mira016

Add driver for the ams OSRAM Mira016 sensor.
quoted hunk ↗ jump to hunk
diff --git a/drivers/media/i2c/mira016.c b/drivers/media/i2c/mira016.c
new file mode 100644
--- /dev/null
+++ b/drivers/media/i2c/mira016.c
@@ -0,0 +1,2284 @@
[ ... ]
+static inline u32 mira016_nsec_to_lines(struct mira016 *mira016, u32 nsec)
+{
+	return nsec / (mira016_trow_psec(mira016) / HZ_PER_KHZ);
+}
+
+static inline u32 mira016_calc_prate(struct mira016 *mira016, u32 h_tot)
+{
+	/*
+	 * Pixel rate is calculate as the row duration divided by the total line
+	 * length.
+	 *
+	 * pixel time (psec) = t_row(psec) / h_tot
+	 * pixel rate (mbps) = 10^12 / pixel_time
+	 *		     = 10^12 * h_tot / t_row
+	 *		     = h_tot * 10^6 / t_row * 10^6 to avoid overflows
+	 */
+	u32 trow_psec = mira016_trow_psec(mira016);
+
+	return h_tot * HZ_PER_MHZ / trow_psec * HZ_PER_MHZ;
[Severity: Medium]
Will this early integer division cause severe precision loss? The formula
h_tot * HZ_PER_MHZ / trow_psec * HZ_PER_MHZ is evaluated left-to-right.
The division by trow_psec truncates the intermediate result to an integer
before it is scaled back up by HZ_PER_MHZ, discarding all sub-megahertz
precision.
+}
+
+static inline u32 mira016_calc_min_vblank(struct mira016 *mira016, u32 y_tot)
+{
+	/*
+	 * See 3.15.2 Frame Rate, equation 4.
+	 *
+	 * TODO: The minimum frame duration has to be expanded if embedded data
+	 * are used.
+	 */
+	u32 trow_nsec = mira016_trow_psec(mira016) / HZ_PER_KHZ;
+	u32 min_duration_nsec =  trow_nsec * (y_tot + 35) + 50 * HZ_PER_KHZ;
+
+	return mira016_nsec_to_lines(mira016, min_duration_nsec) - y_tot;
[Severity: Medium]
Can this result in a vertical blanking duration that is strictly shorter than
the hardware's minimum requirement? mira016_calc_min_vblank() computes a
minimum duration in nanoseconds and converts it to lines using
mira016_nsec_to_lines(). That helper uses integer division, which rounds
down. If the required minimum translates to a fractional number of lines,
the truncated result could violate the minimum duration.
+}
[ ... ]
+static int mira016_calc_row_length(struct mira016 *mira016,
+				   struct v4l2_subdev_state *state)
+{
+	struct v4l2_mbus_framefmt *fmt = v4l2_subdev_state_get_format(state, 0);
+	u32 phy_timings = mira016_calc_phy_timings(mira016, fmt);
+	u32 adc_timings = mira016_calc_adc_timings(mira016, fmt);
+
+	mira016->timings.row_length = max(phy_timings, adc_timings);
[Severity: High]
Does this corrupt the active streaming configuration?

When userspace opens the V4L2 subdevice node, the V4L2 core allocates a new
TRY state and calls the driver's init_state callback. The mira016_init_state()
callback unconditionally calls mira016_calc_row_length(), which reads the
format from the newly created TRY state and directly overwrites the
driver-wide global variable mira016->timings.row_length.

This could reset the active device row length to match the default 8-bit
format even if the camera is currently streaming in a 10-bit or 12-bit format.
+
+	return 0;
+}
+
+static int mira016_set_pad_format(struct v4l2_subdev *sd,
+				  struct v4l2_subdev_state *state,
+				  struct v4l2_subdev_format *fmt)
+{
+	struct mira016 *mira016 = to_mira016(sd);
+	struct v4l2_rect *crop;
+	u32 pixel_rate;
+	u32 min_vblank;
+	u32 gain_max;
+	int ret;
+
+	mira016_update_pad_format(mira016, &fmt->format, fmt->format.code);
+	*v4l2_subdev_state_get_format(state, 0) = fmt->format;
+
+	crop = v4l2_subdev_state_get_crop(state, 0);
+	crop->width = fmt->format.width;
+	crop->height = fmt->format.height;
+	crop->left = MIRA016_PIXEL_ARRAY_LEFT;
+	crop->top = MIRA016_PIXEL_ARRAY_TOP;
+
+	if (fmt->which == V4L2_SUBDEV_FORMAT_TRY)
+		return 0;
+
+	/*
+	 * Update the row length: changing the image format implies changing the
+	 * row_length parameter, which changes the line duration and the pixel
+	 * rate consequentially. Also, changing the image format changes the
+	 * analogue gain limits.
+	 *
+	 * Do not allow to change image format while the subdevice is streaming.
+	 *
+	 * TODO: row length depends on binning, update it also in the
+	 * implementation of set_selection.
+	 */
+	if (v4l2_subdev_is_streaming(sd))
+		return -EBUSY;
[Severity: Medium]
Does this permanently alter the active state before validating if it is safe
to do so?

The subdevice state's format and crop configuration are immediately
overwritten with user-supplied values. Only after these changes does the
function check if the device is streaming and return -EBUSY. This leaves the
internal software state desynchronized from the actual hardware configuration.
+
+	switch (fmt->format.code) {
+	case MEDIA_BUS_FMT_Y8_1X8:
+		gain_max = ARRAY_SIZE(mira016_gain_lut_8bit);
+		break;
+	case MEDIA_BUS_FMT_Y10_1X10:
+		gain_max = ARRAY_SIZE(mira016_gain_lut_10bit);
+		break;
+	case MEDIA_BUS_FMT_Y12_1X12:
+	default:
+		/*
+		 * TODO: Clarify how to handle 12 bit 2x fixed gain which
+		 * changes the line timings while streaming. Only allow 1x
+		 * for the time being.
+		 */
+		gain_max = 1;
+		break;
+	}
+
+	ret = __v4l2_ctrl_modify_range(mira016->gain, 1, gain_max, 1, 1);
[Severity: High]
Does this set up an out-of-bounds array access? The V4L2 control's maximum
value is set to ARRAY_SIZE(...) rather than ARRAY_SIZE(...) - 1. When
mira016_write_analogue_gain() is called with this maximum value, it uses the
1-based maximum directly as a 0-based array index, reading past the end of
the gain lookup table.

[ ... ]
+static int mira016_configure_timings(struct mira016 *mira016)
+{
+	int ret = 0;
+	u64 val;
+
+	/* Configure the PLL clock tree. */
+
+	cci_write(mira016->regmap, MIRA016_BANK_SEL_REG, 0, &ret);
+
+	cci_write(mira016->regmap, MIRA016_PLL_DIV_N_REG, mira016->pll.n, &ret);
+	cci_write(mira016->regmap, MIRA016_PLL_DIV_M_REG, mira016->pll.m, &ret);
+	cci_write(mira016->regmap, MIRA016_CLKGEN_CP_DIV_REG,
+		  mira016->pll.cp_div, &ret);
+	cci_write(mira016->regmap, MIRA016_OTP_GRANULARITY_REG,
+		  mira016->pll.otp_gran, &ret);
+	cci_write(mira016->regmap, MIRA016_GRAN_TG_REG, mira016->pll.gran_tg, &ret);
+	cci_write(mira016->regmap, MIRA016_LUT_DEL008_REG,
+		  mira016->pll.lut_del, &ret);
+	cci_write(mira016->regmap, MIRA016_CLKGEN_TX_ESC_H_REG,
+		  mira016->pll.esc_h, &ret);
+	cci_write(mira016->regmap, MIRA016_CLKGEN_TX_ESC_L_REG,
+		  mira016->pll.esc_l, &ret);
+	cci_write(mira016->regmap, MIRA016_PLL_PD_REG, 0, &ret);
+
+	ret = read_poll_timeout(cci_read, ret,
+				((ret < 0 || (val & MIRA016_PLL_LOCKED))),
+				1000, 1000, false, mira016->regmap,
+				MIRA016_PLL_LOCK_REG, &val, NULL);
[Severity: Medium]
Does this overwrite the accumulated error code in ret? The read_poll_timeout
assigns the result of cci_read directly to ret, replacing any prior errors
accumulated during the cci_write() sequence.

[ ... ]
+static int mira016_configure_roi(struct mira016 *mira016,
+				 struct v4l2_subdev_state *state)
+{
+	int ret = 0;
+
+	/*
+	 * Write horizontal configuration to context 0 first, then repeat for
+	 * context 1.
+	 */
+	cci_write(mira016->regmap, MIRA016_RW_CONTEXT_REG, 0, &ret);
+	cci_write(mira016->regmap, MIRA016_BANK_SEL_REG, 1, &ret);
+	ret = mira016_configure_horizontal_roi(mira016, state);
[Severity: Medium]
Is the error code from the preceding cci_write() calls silently lost here?
cci_write() accumulates errors into ret, but the assignment
ret = mira016_configure_horizontal_roi(...) completely overwrites ret,
discarding prior failures.
+	if (ret)
+		return ret;
[ ... ]
+static int mira016_init_state(struct v4l2_subdev *sd,
+			      struct v4l2_subdev_state *state)
+{
+	struct mira016 *mira016 = to_mira016(sd);
+	struct v4l2_subdev_format fmt = {
+		.which = V4L2_SUBDEV_FORMAT_TRY,
+		.pad = 0,
+		.format = {
+			.code = MEDIA_BUS_FMT_Y8_1X8,
+			.width = MIRA016_PIXEL_ARRAY_WIDTH,
+			.height = MIRA016_PIXEL_ARRAY_HEIGHT
+		},
+	};
+
+	mira016_set_pad_format(sd, state, &fmt);
+	mira016_calc_row_length(mira016, state);
[Severity: High]
As mentioned earlier in mira016_calc_row_length(), this call from
mira016_init_state() mutates the driver-wide global state when userspace
allocates a new TRY state.
+
+	return 0;
+}
[ ... ]
+static int mira016_write_analogue_gain(struct mira016 *mira016,
+				       struct v4l2_subdev_state *state,
+				       u32 gain)
+{
+	const struct mira016_gain_lut *lut;
+	struct v4l2_mbus_framefmt *format;
+	int ret = 0;
+
+	format = v4l2_subdev_state_get_format(state, 0);
+	switch (format->code) {
+	case MEDIA_BUS_FMT_Y8_1X8:
+		lut = &mira016_gain_lut_8bit[gain];
[Severity: High]
If gain matches the maximum array size limit established in
mira016_set_pad_format(), this will read memory past the end of the gain
lookup table.

[ ... ]
+static int mira016_set_ctrl(struct v4l2_ctrl *ctrl)
+{
+	struct mira016 *mira016 =
+		container_of(ctrl->handler, struct mira016, ctrl_handler);
+	struct i2c_client *client = v4l2_get_subdevdata(&mira016->sd);
+	struct v4l2_subdev_state *state;
+	struct v4l2_rect *crop;
+	int ret = 0;
+
+	state = v4l2_subdev_get_locked_active_state(&mira016->sd);
[Severity: High]
Could this sequence lead to a use-after-free or NULL pointer dereference
during driver removal?

In mira016_remove(), v4l2_subdev_cleanup() is called before
v4l2_ctrl_handler_free(). The v4l2_subdev_cleanup() frees the subdevice's
active state, but the control handler remains alive and accessible.

If a concurrent ioctl sets a control, it invokes mira016_set_ctrl(), which
calls v4l2_subdev_get_locked_active_state() and dereferences the already
freed state object.
+	crop = v4l2_subdev_state_get_crop(state, 0);
[ ... ]
+static void mira016_remove(struct i2c_client *client)
+{
+	struct v4l2_subdev *sd = i2c_get_clientdata(client);
+	struct mira016 *mira016 = to_mira016(sd);
+
+	v4l2_async_unregister_subdev(sd);
+	v4l2_subdev_cleanup(&mira016->sd);
+	media_entity_cleanup(&sd->entity);
+
+	v4l2_ctrl_handler_free(mira016->sd.ctrl_handler);
[Severity: High]
As mentioned above, the active_state is destroyed by v4l2_subdev_cleanup()
while the ctrl_handler is still active and capable of receiving ioctls.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260903-mira016-v1-0-af0013ef070d@ideasonboard.com?part=2
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help