Devicetree
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Luo Liu" <Luo.Liu@cixtech.com>
Cc: dri-devel@lists.freedesktop.org, devicetree@vger.kernel.org,
	robh@kernel.org, conor+dt@kernel.org
Subject: Re: [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver
Date: Mon, 09 Nov 2026 10:55:44 +0000	[thread overview]
Message-ID: <sashiko-outbox-204854@kernel.org> (raw)
In-Reply-To: <20261109103010.110911-3-Luo.Liu@cixtech.com>

Thank you for your contribution! Sashiko AI review found 16 potential
issues to consider:
- [High] drm/cix: cix_dp_replay_deferred_hotplug() always fails with
  -ENODEV
- [High] drm/cix: sink DPCD caps not read before detect, mode_valid, and
  mode_set
- [High] drm/cix: PHY ref leak and wedged state in
  cix_dp_encoder_enable() and prepare
- [High] drm/cix: HPD work cancellation and IRQ ordering races on
  teardown and suspend
- [High] drm/cix: unclocked MMIO reads in cix_dp_aux_transfer() and HPD
  workers
- [High] drm/cix/dptx: NULL dereference in cix_dp_panel_hw_cfg() with
  NO_CONNECTOR
- [High] drm/cix/dptx: double connector cleanup and UAF in
  cix_dp_bridge_detach()
- [High] drm/cix/dptx: broken sync flags and YCbCr 4:2:0 selection in
  atomic_check
- [Medium] drm/cix: unbalanced PHY and clock teardown in
  cix_dptx_shutdown() and remove
- [Medium] drm/cix: unbalanced disable_irq() and lost force_hpd state
  across suspend
- [Medium] drm/cix: broken VSC SDP payload and stream override in
  dptx_setup_vsc_sdp()
- [Medium] drm/cix: cix_dp_aux_cmd_submit() reads FIFO and continues on
  NACK or DEFER
- [Medium] drm/cix/dptx: unchecked state allocation in
  cix_dp_connector_create()
- [Medium] drm/cix/dptx: ignored mode_set failure causes divide-by-zero
  on enable
- [Medium] drm/cix/dptx: CIX_DPTX_ENHANCED_FRAMING_ENABLE not cleared
  when disabled
- [Medium] drm/cix/dptx: unlocked access to shared state in mode_set and
  oob_hotplug
--

commit a2a34412966dc5d33f9669af4c794727d4e8b1e4
Author: Luo Liu <Luo.Liu@cixtech.com>

drm/cix: add Sky1 DisplayPort transmitter driver

This commit adds a DRM bridge driver for the CIX Sky1 DisplayPort
transmitter, implementing DP link training, AUX transfers, hotplug
handling, and connector management.

> diff --git a/drivers/gpu/drm/cix/dptx/cix_dptx.c b/drivers/gpu/drm/cix/dptx/cix_dptx.c
> new file mode 100644
> index 0000000000000..8915c50bc9db3
> --- /dev/null
> +++ b/drivers/gpu/drm/cix/dptx/cix_dptx.c

[ ... ]

> @@ -0,0 +1,2086 @@
> +static int cix_dp_link_configure(struct cix_dp *dp)
> +{
> +	u8 bw_code = dp->mode.bw_code;
> +	u8 lane_cnt = dp->mode.lane_cnt;
> +	int ret;
> +
> +	cix_dp_write(dp, CIX_DPTX_LANE_COUNT_SET, lane_cnt);
> +
> +	if (dp->caps.enhanced_framing) {
> +		cix_dp_write(dp, CIX_DPTX_ENHANCED_FRAMING_ENABLE, 1);
> +		lane_cnt |= DP_LANE_COUNT_ENHANCED_FRAME_EN;
> +	}

[Severity: Medium]
Should CIX_DPTX_ENHANCED_FRAMING_ENABLE be cleared when
dp->caps.enhanced_framing is false?  If a sink without enhanced framing is
connected after one that enabled it, the transmitter keeps sending enhanced
framing while DP_LANE_COUNT_ENHANCED_FRAME_EN is cleared in the sink DPCD.

[ ... ]

> +	value = cix_dp_read(dp, CIX_DPTX_AUX_REPLY_CODE);
> +	if (reply)
> +		*reply = value;
> +
> +	if (is_read) {
> +		if (value == CIX_DPTX_AUX_REPLY_CODE_AUX_ACK ||
> +		    value == CIX_DPTX_AUX_REPLY_CODE_I2C_ACK) {
> +			value = cix_dp_read(dp, CIX_DPTX_AUX_REPLY_DATA_COUNT);
> +			if ((value & CIX_DPTX_REPLY_DATA_COUNT_MASK) != bytes)
> +				return -EIO;
> +		}
> +		for (i = 0; i < bytes; i++)
> +			buf[i] = cix_dp_read(dp, CIX_DPTX_AUX_REPLY_DATA);
> +	}
> +
> +	return 0;
> +}
> +
> +static ssize_t cix_dp_aux_transfer(struct drm_dp_aux *aux,
> +				   struct drm_dp_aux_msg *msg)
> +{
> +	struct cix_dp *dp = container_of(aux, struct cix_dp, aux);
> +	size_t to_access = 0;
> +	size_t accessed_bytes = 0;
> +	int ret;
> +
> +	if (!cix_dp_get_hpd_state(dp))
> +		return -ENODEV;
> +
> +	do {
> +		to_access = min_t(size_t, DP_AUX_MAX_PAYLOAD_BYTES,
> +				  msg->size - accessed_bytes);
> +		ret = cix_dp_aux_cmd_submit(dp, msg->request,
> +					    msg->address + accessed_bytes,
> +					    msg->buffer + accessed_bytes,
> +					    to_access, &msg->reply);
> +		if (ret)
> +			return accessed_bytes ? (ssize_t)accessed_bytes : ret;
> +
> +		accessed_bytes += to_access;
> +	} while (accessed_bytes < msg->size);
> +
> +	return msg->size;
> +}

[Severity: Medium]
Does cix_dp_aux_cmd_submit() read from an empty CIX_DPTX_AUX_REPLY_DATA
FIFO when the sink replies with NACK or DEFER?

When value is non-zero (NACK or DEFER), the reply data count check is
skipped, but the loop still reads bytes entries from
CIX_DPTX_AUX_REPLY_DATA into buf and returns 0.  In cix_dp_aux_transfer(),
multi-chunk transfers also keep submitting subsequent chunks after a NACK
or DEFER instead of stopping and returning the reply code to the DRM AUX
helper.

[ ... ]

> +static int dptx_setup_vsc_sdp(struct dp_sdp *sdp,
> +			      struct cix_dp_config *config)
> +{
> +	sdp->sdp_header.HB0 = 0;
> +	sdp->sdp_header.HB1 = 0x7;
> +	sdp->sdp_header.HB2 = 0x5;
> +	sdp->sdp_header.HB3 = 0x13;
> +
> +	switch (config->format) {
> +	case CIX_DP_FORMAT_YCBCR444:
> +		sdp->db[16] = 0x1 << 4;
> +		break;
> +	case CIX_DP_FORMAT_YCBCR422:
> +		sdp->db[16] = 0x1 << 4;
> +		break;
> +	case CIX_DP_FORMAT_YCBCR420:
> +		sdp->db[16] = 0x3 << 4;
> +		break;
> +	default:
> +		break;
> +	}
> +
> +	switch (config->colorspace) {
> +	case DRM_MODE_COLORIMETRY_BT709_YCC:
> +		sdp->db[16] |= 0x1;

[ ... ]

> +	switch (config->bpc) {
> +	case 16:
> +		sdp->db[17] = 0x4;
> +		break;
> +	case 12:
> +		sdp->db[17] = 0x3;
> +		break;
> +	case 10:
> +		sdp->db[17] = 0x2;
> +		break;
> +	case 8:
> +	default:
> +		sdp->db[17] = 0x1;
> +		break;
> +	}
> +
> +	/* Dynamic Range and Component Bit Depth */
> +	if (config->dynamic_range == DP_DYNAMIC_RANGE_CTA)
> +		sdp->db[17] |= 0x80; /* DB17[7] */
> +
> +	sdp->db[18] = config->content_type & 0x7;
> +
> +	return 0;
> +}

[Severity: Medium]
Does dptx_setup_vsc_sdp() program the wrong pixel encoding and bit depth in
the VSC SDP payload?

CIX_DP_FORMAT_YCBCR422 sets sdp->db[16] to 0x1 << 4 (YCbCr 4:4:4) instead
of 0x2 << 4 (YCbCr 4:2:2).  On the default (RGB) path, sdp->db[16] is not
cleared before ORing the colorimetry bits, leaving stale format and
colorimetry values from earlier modesets.  In addition, 6 bpc falls into
the default branch and encodes sdp->db[17] as 0x1 (8 bpc) instead of 0x0.

In cix_dp_set_misc(), the VSC SDP path also only sets the pixel format
override for CIX_DP_FORMAT_YCBCR420 (leaving override format bits as RGB
for YCbCr 4:2:2 and 4:4:4) and hardcodes config->content_type to
DP_CONTENT_TYPE_NOT_DEFINED instead of using the connector's content_type
property.

> +
> +static int cix_dp_panel_setup_colorimetry_sdp(struct cix_dp *dp,
> +					      struct cix_dp_panel *dp_panel)
> +{
> +	struct cix_connector *conn = dp_panel->connector;
> +
> +	if (conn->config.override & CIX_DPTX_STREAM_OVERRIDE_ENABLE) {
> +		dptx_setup_vsc_sdp(&conn->sdp[CIX_VSC_SDP], &conn->config);
> +		infoframe_write_packet(dp, dp_panel->source_id, CIX_VSC_SDP,
> +				       &conn->sdp[CIX_VSC_SDP], 1);
> +	}
> +
> +	return 0;
> +}

[ ... ]

> +static int cix_dp_panel_hw_cfg(struct cix_dp *dp, int stream_id)
> +{
> +	struct cix_dp_panel *dp_panel = dp->active_panels[stream_id];
> +	struct drm_display_mode *mode = cix_dp_get_mode(dp, stream_id);
> +	struct cix_connector *conn;
> +
> +	if (!mode)
> +		return -EINVAL;
> +
> +	conn = dp_panel->connector;
> +
> +	cix_dp_cfg_adapter(dp, stream_id);
> +
> +	cix_dp_set_misc(dp, dp_panel);
> +	cix_dp_panel_setup_colorimetry_sdp(dp, dp_panel);
> +
> +	cix_dp_mode_set_transfer_unit(dp, dp_panel->source_id, mode,
> +				      conn->config.bpp);
> +	cix_dp_mode_set_timing(dp, dp_panel->source_id, mode, conn->config.bpp);
> +	return 0;
> +}

[Severity: High]
Can cix_dp_panel_hw_cfg() and cix_dp_panel_setup_colorimetry_sdp()
dereference a NULL dp_panel->connector when the bridge is attached with
DRM_BRIDGE_ATTACH_NO_CONNECTOR?

When DRM_BRIDGE_ATTACH_NO_CONNECTOR is passed to cix_dp_bridge_attach(),
cix_dp_connector_create() is skipped and dp->dp_panel.connector remains
NULL.  Enabling the bridge then calls cix_dp_panel_hw_cfg(), which ignores
the -EINVAL return from cix_dp_set_misc() and dereferences conn in
cix_dp_panel_setup_colorimetry_sdp() and cix_dp_mode_set_transfer_unit().
In addition, cix_dp_hpd_event_work_func() returns early when
dp->dp_panel.connector is NULL, and cix_dp_bridge_funcs does not implement
the bridge detection or EDID callbacks needed when connector creation is
delegated to the display controller.

[ ... ]

> +static void cix_dp_replay_deferred_hotplug(struct cix_dp *dp)
> +{
> +	struct phy *phy = dp->phy;
> +	union phy_configure_opts opts = { 0 };
> +	u8 dpcd_rev;
> +
> +	if (!(dp->state & DPTX_STATE_INITIALIZED))
> +		return;
> +	if (dp->force_hpd != connector_status_unknown)
> +		return;
> +	if (cix_dp_get_hpd_state(dp))
> +		return;
> +
> +	phy_validate(phy, PHY_MODE_DP, 0, &opts);
> +	if (phy_get_mode(phy) != PHY_MODE_DP)
> +		return;
> +
> +	/*
> +	 * The combo PHY is already muxed to DP, so the sink may have been
> +	 * plugged in before the connector existed. Probe the AUX channel to
> +	 * make sure one is really there before forcing a hotplug event.
> +	 */
> +	if (drm_dp_dpcd_readb(&dp->aux, DP_DPCD_REV, &dpcd_rev) < 0)
> +		return;
> +
> +	dp->force_hpd = connector_status_connected;
> +	dp->status = connector_status_unknown;
> +	schedule_delayed_work(&dp->hpd_event_work, 0);
> +}

[Severity: High]
Does drm_dp_dpcd_readb() in cix_dp_replay_deferred_hotplug() always fail
with -ENODEV?

cix_dp_replay_deferred_hotplug() returns early if cix_dp_get_hpd_state(dp)
is true, so reaching drm_dp_dpcd_readb() guarantees cix_dp_get_hpd_state(dp)
is false.  Because dp->force_hpd is only set to connector_status_connected
after drm_dp_dpcd_readb(), cix_dp_aux_transfer() sees
!cix_dp_get_hpd_state(dp) and rejects the AUX read with -ENODEV, preventing
deferred hotplug from ever triggering.

> +
> +static void cix_dp_hpd_event_work_func(struct work_struct *work)
> +{
> +	struct cix_dp *dp = container_of(work, struct cix_dp,
> +					 hpd_event_work.work);
> +	struct phy *phy = dp->phy;
> +	union phy_configure_opts opts = { 0 };
> +	enum drm_connector_status old_status;
> +	enum phy_mode mode;
> +	int i;
> +
> +	if (!dp->dp_panel.connector)
> +		return;
> +
> +	/*
> +	 * On a USB-C DP alt-mode PHY the hotplug only becomes valid once the
> +	 * PHY has been switched to DP mode, so wait for the mode change.
> +	 * A native DP PHY has no ->set_mode() operation, which leaves the
> +	 * mode at PHY_MODE_INVALID forever; there is nothing to wait for.
> +	 */
> +	if (cix_dp_get_hpd_state(dp) &&
> +	    phy_get_mode(phy) != PHY_MODE_INVALID) {
> +		for (i = 0; i < DP_HPD_MAX_TRIES; i++) {
> +			phy_validate(phy, PHY_MODE_DP, 0, &opts);
> +			mode = phy_get_mode(phy);
> +			if (mode == PHY_MODE_DP)
> +				break;
> +			msleep(100);
> +		}
> +	}
> +
> +	old_status = dp->status;
> +	drm_helper_probe_detect(&dp->connector.base, NULL, false);
> +
> +	if (dp->status == connector_status_connected)
> +		cix_dp_handle_connect(dp);
> +	else
> +		cix_dp_handle_disconnect(dp);
> +
> +	if (old_status != dp->status && dp->drm)
> +		drm_helper_hpd_irq_event(dp->drm);
> +}

[Severity: High]
Does probing the connector before cix_dp_handle_connect() leave dp->dpcd,
dp->link_config, and dp->caps uninitialized during detection and mode
validation?

cix_dp_detect() only checks the HPD pin level and never reads DPCD.  In
cix_dp_hpd_event_work_func(), drm_helper_probe_detect() runs before
cix_dp_handle_connect(), so cix_dp_connector_detect() passes zeroed or
stale dp->dpcd and dp->downstream_ports to
drm_dp_set_subconnector_property().  Likewise, when a native DP monitor is
connected before cix_dp_bridge_attach() creates the connector, or after
system resume, DRM probes the connector and validates modes before
cix_dp_handle_connect() ever runs, and cix_dp_bridge_atomic_enable() calls
cix_dp_bridge_mode_set() before encoder_enable() reads the sink's DPCD
capabilities.

Also, in cix_dp_handle_connect(), DPTX_STATE_CONNECTED is only cleared when
cix_dp_panel_read_sink_caps() fails with -ETIMEDOUT or -ENOTCONN; if it
fails with -EIO or -EBUSY, DPTX_STATE_CONNECTED remains set with zeroed
caps and blocks later retries.

> +
> +static void cix_dp_hpd_irq_work_func(struct work_struct *work)
> +{
> +	struct cix_dp *dp = container_of(work, struct cix_dp,
> +					 hpd_irq_work.work);
> +	u8 status[DP_LINK_STATUS_SIZE + 2];
> +	int ret;
> +
> +	if (!cix_dp_get_hpd_state(dp))
> +		return;
> +
> +	mutex_lock(&dp->session_lock);
> +
> +	if (!(dp->state & DPTX_STATE_INITIALIZED) ||
> +	    !(dp->state & DPTX_STATE_ENABLED))
> +		goto end;

[Severity: High]
Can cix_dp_get_hpd_state() and cix_dp_aux_transfer() read DPTX registers
while dp->apb_clk is gated?

As noted above cix_dp_detect(), cix_dp_read() hangs the bus if called when
DPTX_STATE_INITIALIZED is clear and dp->apb_clk is disabled.
cix_dp_hpd_irq_work_func() calls cix_dp_get_hpd_state() before locking
dp->session_lock and checking DPTX_STATE_INITIALIZED, and
cix_dp_aux_transfer() (reachable via /dev/drm_dp_aux*) and
cix_dp_hpd_event_work_func() access DPTX registers without checking
DPTX_STATE_INITIALIZED under dp->session_lock.

[ ... ]

> +static int cix_dp_pm_complete(struct cix_dp *dp)
> +{
> +	int ret = 0;
> +
> +	mutex_lock(&dp->session_lock);
> +
> +	if (!(dp->state & DPTX_STATE_SUSPENDED))
> +		goto end;
> +
> +	ret = cix_dp_host_init(dp);
> +	if (ret)
> +		goto end;
> +
> +	enable_irq(dp->irq);
> +	dp->state &= ~DPTX_STATE_SUSPENDED;
> +
> +	if (dp->drm && dp->drm->mode_config.poll_enabled)
> +		drm_kms_helper_poll_enable(dp->drm);
> +end:
> +	mutex_unlock(&dp->session_lock);
> +	return ret;
> +}

[Severity: Medium]
If cix_dp_host_init() fails in cix_dp_pm_complete(), does dp->irq stay
disabled while a later cix_dp_pm_prepare() or cix_dp_deinit() calls
disable_irq() a second time?

Because cix_dp_pm_prepare() and cix_dp_deinit() call disable_irq(dp->irq)
unconditionally without checking whether DPTX_STATE_SUSPENDED is already
set, a failed resume nests the IRQ disable depth.  Additionally, when
DPTX_STATE_SUSPENDED is set, cix_dp_oob_hotplug_event() overwrites
dp->force_hpd with connector_status_disconnected instead of recording
hpd_state, losing Type-C plug events that arrive during suspend.

> +
> +static int cix_dp_prepare(struct cix_dp *dp)
> +{
> +	int ret;
> +
> +	mutex_lock(&dp->session_lock);
> +
> +	if (dp->state & (DPTX_STATE_ENABLED)) {
> +		ret = 0;
> +		goto end;
> +	}
> +
> +	if (!(dp->state & DPTX_STATE_INITIALIZED)) {
> +		ret = cix_dp_host_init(dp);
> +		if (ret)
> +			goto end;
> +	} else if (dp->state & DPTX_STATE_INIT_TRAIN) {
> +		reset_dp_and_reinit(dp);
> +		usleep_range(100, 200);
> +	}
> +
> +	if (cix_dp_get_hpd_state(dp) &&
> +	    !(dp->state & DPTX_STATE_CONNECTED)) {
> +		mutex_unlock(&dp->session_lock);
> +		cix_dp_handle_connect(dp);
> +		mutex_lock(&dp->session_lock);
> +	}
> +
> +	if (!(cix_dp_get_hpd_state(dp) && (dp->state & DPTX_STATE_CONNECTED))) {
> +		ret = 0;
> +		goto end;
> +	}
> +
> +	ret = cix_dp_core_on(dp, true);
> +
> +end:
> +	mutex_unlock(&dp->session_lock);
> +	return ret;
> +}

[ ... ]

> +static int cix_dp_encoder_enable(struct cix_dp *dp, int stream_id, int enable)
> +{
> +	int ret;
> +
> +	if (enable) {
> +		ret = cix_dp_prepare(dp);
> +		if (ret)
> +			return ret;
> +
> +		ret = cix_dp_enable(dp, stream_id);
> +	} else {
> +		if (!(dp->state & DPTX_STATE_INITIALIZED))
> +			return 0;
> +
> +		ret = cix_dp_disable(dp, stream_id);
> +		if (ret)
> +			return ret;
> +
> +		ret = cix_dp_unprepare(dp);
> +	}
> +
> +	return ret;
> +}

[Severity: High]
Does a failure in cix_dp_enable() leak PHY references and leave the
transmitter wedged with DPTX_STATE_READY set?

In cix_dp_encoder_enable(), cix_dp_prepare() calls cix_dp_core_on(), which
calls phy_init() and phy_power_on() and sets DPTX_STATE_READY |
DPTX_STATE_INIT_TRAIN.  If cix_dp_enable() then fails, DPTX_STATE_ENABLED
is not set and cix_dp_core_off() is not called.  A subsequent disable will
return early in cix_dp_unprepare() because DPTX_STATE_ENABLED is clear,
while a subsequent enable will call reset_dp_and_reinit() (leaking another
phy_init() call) and then return early in cix_dp_core_on() because
DPTX_STATE_READY is still set, skipping link training after the reset.

Also, if the display controller resumes and calls cix_dp_prepare() before
cix_dptx_pm_resume() clears DPTX_STATE_SUSPENDED, cix_dp_handle_connect()
will bail out on DPTX_STATE_SUSPENDED and leave the display disabled.

[ ... ]

> +static void cix_dp_deinit(struct cix_dp *dp)
> +{
> +	cancel_delayed_work_sync(&dp->sw_hpd_irq_work);
> +	cancel_delayed_work_sync(&dp->hpd_irq_work);
> +	cancel_delayed_work_sync(&dp->hpd_event_work);
> +	disable_irq(dp->irq);
> +
> +	mutex_lock(&dp->session_lock);
> +	cix_dp_host_deinit(dp);
> +	dp->state &= ~(DPTX_STATE_CONFIGURED);
> +	mutex_unlock(&dp->session_lock);
> +
> +	mutex_destroy(&dp->session_lock);
> +}

[Severity: High]
Can HPD delayed work items be re-queued after cancel_delayed_work_sync()
during teardown, bridge detach, or suspend?

CPU0                              CPU1
cix_dp_deinit()
  cancel_delayed_work_sync(...)
                                  cix_dp_irq_handler()
                                    queue_delayed_work(...,
                                      &dp->hpd_event_work, 0)
  disable_irq(dp->irq)
  cix_dp_host_deinit(dp)
  mutex_destroy(&dp->session_lock)
                                  cix_dp_hpd_event_work_func()
                                    <-- unclocked MMIO / UAF

In cix_dp_deinit(), disable_irq(dp->irq) is called after
cancel_delayed_work_sync().  In cix_dp_bridge_detach(), hpd_irq_work is
cancelled before sw_hpd_irq_work (which queues hpd_irq_work), while dp->irq
remains enabled and dp->dp_panel.connector is only cleared after
drm_connector_cleanup().  In cix_dp_pm_prepare(), the works are cancelled
before DPTX_STATE_SUSPENDED is set, allowing a concurrent
cix_dp_oob_hotplug_event() to schedule them right before cix_dp_host_deinit()
gates dp->apb_clk.

[ ... ]

> +static void cix_dptx_shutdown(struct platform_device *pdev)
> +{
> +	struct cix_dp *dp = platform_get_drvdata(pdev);
> +
> +	if (dp) {
> +		mutex_lock(&dp->session_lock);
> +		dp->active_stream_cnt = 0;
> +		mutex_unlock(&dp->session_lock);
> +		cix_dp_pm_prepare(dp);
> +	}
> +}

[Severity: Medium]
Does zeroing dp->active_stream_cnt in cix_dptx_shutdown() cause
cix_dp_host_deinit() to call phy_exit() while the PHY is still powered on?

When a stream is active at shutdown, forcing dp->active_stream_cnt = 0
without calling cix_dp_ctrl_stream_off() and cix_dp_core_off() leaves
stream_clk enabled and the PHY powered on when cix_dp_pm_prepare() ->
cix_dp_host_deinit() calls phy_exit() and disables dp->apb_clk.
Conversely, in cix_dptx_remove() -> cix_dp_deinit(), if
dp->active_stream_cnt is non-zero, cix_dp_host_deinit() returns early and
leaks the clocks and PHY references.

> diff --git a/drivers/gpu/drm/cix/dptx/cix_dptx_drm.c b/drivers/gpu/drm/cix/dptx/cix_dptx_drm.c
> new file mode 100644
> index 0000000000000..0ddcd76691e03
> --- /dev/null
> +++ b/drivers/gpu/drm/cix/dptx/cix_dptx_drm.c

[ ... ]

> @@ -0,0 +1,653 @@
> +static void cix_dp_adjust_mode(struct drm_display_mode *mode,
> +			       struct drm_display_mode *adjusted_mode)
> +{
> +	int diff = mode->htotal - mode->hsync_end;
> +
> +	/*
> +	 * Cix DP requires horizontal backporch to be greater than 12.
> +	 * This limitation may not be compatible with the sink device.
> +	 *
> +	 * Everything below is derived from @mode instead of accumulating on
> +	 * @adjusted_mode, so that repeated atomic checks of the same state
> +	 * produce the same result.
> +	 */
> +	if (diff < CIX_DPTX_MIN_H_BACKPORCH) {
> +		adjusted_mode->htotal = mode->htotal +
> +					CIX_DPTX_MIN_H_BACKPORCH - diff;
> +		adjusted_mode->clock = (int64_t)mode->clock *
> +				       adjusted_mode->htotal / mode->htotal;
> +	}
> +
> +	/* The sync polarities are inverted with respect to the source mode. */\n> +	if (mode->flags & DRM_MODE_FLAG_PVSYNC)
> +		adjusted_mode->flags &= ~DRM_MODE_FLAG_PVSYNC;
> +	else
> +		adjusted_mode->flags |= DRM_MODE_FLAG_PVSYNC;
> +
> +	if (mode->flags & DRM_MODE_FLAG_PHSYNC)
> +		adjusted_mode->flags &= ~DRM_MODE_FLAG_PHSYNC;
> +	else
> +		adjusted_mode->flags |= DRM_MODE_FLAG_PHSYNC;
> +}

[ ... ]

> +static int cix_dp_pick_format_bpc(struct cix_dp *dp,
> +				  struct drm_connector_state *conn_state,
> +				  struct drm_display_mode *adjusted_mode,
> +				  int info_formats, int suggest_bpc,
> +				  int *color_format, int *bpc, int *bpp)
> +{
> +	struct drm_display_info *info =
> +		&conn_state->connector->display_info;
> +	int curr_bpc;
> +	int format;
> +	int i;
> +
> +	for (i = 0; i < ARRAY_SIZE(fmt_prio); i++) {
> +		format = fmt_prio[i].format;
> +		curr_bpc = fmt_prio[i].bpc ? fmt_prio[i].bpc : suggest_bpc;
> +
> +		/* Only use 420_only flag for YUV420 format */
> +		if (!(info_formats & BIT(format)) &&
> +		    !(format == DRM_OUTPUT_COLOR_FORMAT_YCBCR420 &&
> +		      drm_mode_is_420_only(info, adjusted_mode)))
> +			continue;

[Severity: High]
Does mutating adjusted_mode->flags in cix_dp_adjust_mode() break CEA mode
matching and YCbCr 4:2:0 format selection in cix_dp_pick_format_bpc()?

cix_dp_adjust_mode() toggles DRM_MODE_FLAG_PVSYNC and DRM_MODE_FLAG_PHSYNC
without updating DRM_MODE_FLAG_NVSYNC and DRM_MODE_FLAG_NHSYNC, leaving
adjusted_mode->flags with either both positive and negative sync flags set
or neither set.  When cix_dp_pick_format_bpc() then passes adjusted_mode to
drm_mode_is_420_only(), drm_match_cea_mode() fails to match the mutated
flags (and any adjusted htotal/clock) and always returns false.

Furthermore, cix_dp_pick_format_bpc() does not skip non-4:2:0 formats when a
mode is 4:2:0-only (so RGB444 or YCbCr 4:2:2 is picked whenever link
bandwidth allows), and it does not check drm_mode_is_420() or
dp->caps.vsc_supported before picking DRM_OUTPUT_COLOR_FORMAT_YCBCR420 (nor
does cix_dp_bridge_atomic_check() validate adjusted_mode->clock against
CIX_DP_MAX_FREQ).

[ ... ]

> +static void cix_dp_bridge_mode_set(struct cix_dp *dp,
> +				   struct drm_crtc_state *crtc_state,
> +				   struct drm_connector_state *conn_state)
> +{
> +	struct drm_display_mode *adjusted_mode = &crtc_state->adjusted_mode;
> +	struct cix_connector *cix_conn = &dp->connector;
> +	int color_format;
> +	int link_rate;
> +	int bw_code;
> +	int bpc;
> +	int bpp;
> +	int ret;
> +
> +	ret = cix_dp_pick_config(dp, conn_state, adjusted_mode,
> +				 &color_format, &bpc, &bpp);
> +	if (ret) {
> +		dev_err(dp->dev, "no usable DP link configuration: %d\n", ret);
> +		return;
> +	}
> +
> +	cix_conn->config.format = cix_dp_drm_format_to_dp(color_format);
> +	cix_conn->config.colorspace = conn_state->colorspace;
> +	cix_conn->config.bpc = bpc;
> +	cix_conn->config.bpp = bpp;

[Severity: Medium]
Can cix_dp_bridge_mode_set() race with cix_dp_hpd_irq_work_func() and
cix_dp_handle_connect() on dp->mode, dp->connector.config, and
dp->link_config?

cix_dp_hpd_irq_work_func() holds dp->session_lock while cix_dp_train_loop()
reads dp->connector.config.bpp and updates dp->mode, and
cix_dp_handle_connect() holds dp->session_lock while updating
dp->link_config, but cix_dp_bridge_mode_set() reads dp->link_config and
writes dp->connector.config and dp->mode without holding dp->session_lock.
Similarly, cix_dp_oob_hotplug_event() reads dp->state and updates
dp->force_hpd without holding dp->session_lock.

> +	dp->pixel_per_cycle = cix_dp_pick_ppc(adjusted_mode,
> +					      cix_conn->config.format);
> +
> +	link_rate = cix_dp_pick_link_rate(dp, adjusted_mode, bpp);
> +	bw_code = drm_dp_link_rate_to_bw_code(link_rate);
> +
> +	dp->ops->mode_configure(dp, adjusted_mode->clock, bw_code, bpp, false);

[Severity: Medium]
If cix_dp_pick_config() or dp->ops->mode_configure() fails in
cix_dp_bridge_mode_set(), can cix_dp_bridge_atomic_enable() divide by zero?

When cix_dp_bridge_mode_set() fails before dp->mode is populated,
cix_dp_bridge_atomic_enable() still calls dp->ops->encoder_enable() with
dp->mode.lane_cnt == 0 and dp->mode.bw_code == 0.  Link training succeeds
vacuously for 0 lanes, and cix_dp_mode_set_transfer_unit() and
cix_dp_mode_set_timing() then divide by dp->mode.lane_cnt.

[ ... ]

> +static int cix_dp_connector_create(struct cix_dp *dp,
> +				   struct drm_encoder *encoder)
> +{
> +	struct cix_connector *conn = &dp->connector;
> +	struct drm_connector *connector = &conn->base;
> +	int ret;
> +
> +	conn->dp = dp;
> +
> +	connector->polled = DRM_CONNECTOR_POLL_HPD;
> +	connector->ycbcr_420_allowed = true;
> +
> +	ret = drm_connector_init_with_ddc(encoder->dev, connector,
> +					  &cix_dp_connector_funcs,
> +					  DRM_MODE_CONNECTOR_DisplayPort,
> +					  &dp->aux.ddc);
> +	if (ret)
> +		return ret;
> +
> +	drm_connector_helper_add(connector, &cix_dp_connector_helper_funcs);
> +
> +	drm_atomic_helper_connector_reset(connector);
> +
> +	ret = drm_connector_attach_encoder(connector, encoder);
> +	if (ret)
> +		goto err_cleanup;
> +
> +	cix_dp_add_properties(connector);

[Severity: Medium]
Can cix_dp_add_properties() dereference a NULL connector->state if the
allocation inside drm_atomic_helper_connector_reset() fails?
drm_connector_attach_max_bpc_property() unconditionally writes to
connector->state->max_requested_bpc and connector->state->max_bpc.

[ ... ]

> +static void cix_dp_bridge_detach(struct drm_bridge *bridge)
> +{
> +	struct cix_dp *dp = bridge_to_dp(bridge);
> +
> +	/* The hotplug workers reach the connector, stop them first. */
> +	cancel_delayed_work_sync(&dp->hpd_event_work);
> +	cancel_delayed_work_sync(&dp->hpd_irq_work);
> +	cancel_delayed_work_sync(&dp->sw_hpd_irq_work);
> +
> +	if (dp->connector.base.dev)
> +		drm_connector_cleanup(&dp->connector.base);

[Severity: High]
Does calling drm_connector_cleanup() in cix_dp_bridge_detach() cause a
double cleanup when the DRM mode_config or an atomic state still holds a
reference to the connector?

cix_dp_connector_funcs sets .destroy = drm_connector_cleanup, so the
connector is reference-counted and cleaned up when its last reference is
dropped (such as during drm_mode_config_cleanup()).  Calling
drm_connector_cleanup() directly in cix_dp_bridge_detach() zeroes the
embedded struct drm_connector while references remain, leading to a
refcount underflow and a NULL connector->funcs->destroy dereference in
drm_connector_put().

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261109103010.110911-1-Luo.Liu@cixtech.com?part=2

  reply	other threads:[~2026-11-09 10:55 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-09 10:30 [PATCH 0/2] drm/cix: add Sky1 DisplayPort transmitter Luo Liu
2026-11-09 10:30 ` [PATCH 1/2] dt-bindings: display: add CIX " Luo Liu
2026-11-09 10:42   ` sashiko-bot
2026-11-09 10:30 ` [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver Luo Liu
2026-11-09 10:55   ` sashiko-bot [this message]
  -- strict thread matches above, loose matches on Subject: below --
2026-11-09  9:26 [PATCH 0/2] drm/cix: add Sky1 DisplayPort transmitter Luo Liu
2026-11-09  9:26 ` [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver Luo Liu
2026-11-09 20:30   ` sashiko-bot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-204854@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=Luo.Liu@cixtech.com \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=dri-devel@lists.freedesktop.org \
    --cc=robh@kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox