From: sashiko-bot@kernel.org
To: "Luo Liu" <Luo.Liu@cixtech.com>,
dri-devel@lists.freedesktop.org, devicetree@vger.kernel.org
Cc: robh@kernel.org, conor+dt@kernel.org
Subject: Re: [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver
Date: Mon, 09 Nov 2026 20:30:05 +0000 [thread overview]
Message-ID: <sashiko-outbox-205583@kernel.org> (raw)
In-Reply-To: <20261109092623.102291-3-Luo.Liu@cixtech.com>
Thank you for your contribution! Sashiko AI review found 16 potential
issues to consider:
- [High] drm/cix: unread sink DPCD caps and status race on boot, resume,
and HPD
- [High] drm/cix: AUX probe in cix_dp_replay_deferred_hotplug() always
fails with -ENODEV
- [High] drm/cix: broken VSC SDP packing, stream override, and YCbCr420
validation
- [High] drm/cix: unclocked MMIO register accesses when apb_clk is gated
- [High] drm/cix: NULL pointer dereference when
DRM_BRIDGE_ATTACH_NO_CONNECTOR is set
- [High] drm/cix: broken YCbCr 4:2:0 mode matching, format selection,
and VSC SDP
- [Medium] drm/cix: PHY refcount leak and state desync when
cix_dp_enable() or resume fails
- [Medium] drm/cix: HPD workqueue and IRQ teardown races in deinit,
detach, and suspend
- [Medium] drm/cix: active streams and PHY power left enabled in
shutdown and remove
- [Medium] drm/cix: display stays off if cix_dp_prepare() runs before
cix_dp_pm_complete()
- [Medium] drm/cix: missing stream TU/timing update and unchecked mode
config on retrain
- [Medium] drm/cix: broken BT.2020 bpc fallback in cix_dp_cal_bpc() and
6 bpc default
- [Medium] drm/cix: unlocked mode state update in
cix_dp_bridge_mode_set() before prepare
- [Medium] drm/cix: FIFO underflow read on AUX NACK/DEFER in
cix_dp_aux_cmd_submit()
- [Medium] drm/cix: plain 64-bit division in cix_dp_adjust_mode() breaks
32-bit builds
- [Low] drm/cix: cix_dp_aux_cmd_submit() reads reply FIFO on AUX/I2C
NACK or DEFER
--
commit 002cf156569e15175af86684eb3509dc08da82b1
Author: Luo Liu <Luo.Liu@cixtech.com>
drm/cix: add Sky1 DisplayPort transmitter driver
This commit adds a DRM bridge and connector driver for the CIX Sky1
DisplayPort transmitter, driving the DisplayPort PHY via the generic PHY
API and supporting link training, EDID probing, YCbCr 4:2:0, and hotplug.
> 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
[ ... ]
> +static int cix_dp_train_loop(struct cix_dp *dp)
> +{
> + struct cix_dp_mode *mode = &dp->mode;
> + u8 bpp = dp->connector.config.bpp;
> + u8 bw_code = mode->bw_code;
> + int i;
> + int j;
> + int ret;
> +
> + for (i = 0; i < DP_MAX_TRAINING_LOOP; i++) {
> + for (j = 0; j <= 3; j++) {
> + if (!(dp->state & DPTX_STATE_READY))
> + return 0;
> +
> + ret = cix_dp_train(dp);
> + if (!ret)
> + return 0;
> +
> + ret = cix_dp_mode_configure(dp, mode->pclock, bw_code,
> + bpp, true);
> + if (ret < 0)
> + break;
> +
> + bw_code = dp->mode.bw_code;
> + }
> +
> + cix_dp_mode_configure(dp, mode->pclock, 0, bpp, true);
> + bw_code = dp->mode.bw_code;
> + }
> +
> + return -EIO;
> +}
[Severity: Medium]
When cix_dp_hpd_irq_work_func() calls cix_dp_train_loop() while a stream is
enabled and link training downshifts dp->mode.bw_code or changes
dp->mode.lane_cnt, does anything reprogram the active stream's transfer unit
and timing registers? cix_dp_mode_set_transfer_unit() and
cix_dp_mode_set_timing() depend on bw_code, link_rate, and lane_cnt, so
leaving CIX_DPTX_TU_CONFIG, CIX_DPTX_USER_DATA_COUNT,
CIX_DPTX_SECONDARY_DATA_WINDOW, and CIX_DPTX_DATA_CONTROL at their old values
corrupts the running video stream. Also, the fallback
cix_dp_mode_configure() call at the end of the outer loop ignores its return
value.
> +
> +static int cix_dp_aux_cmd_submit(struct cix_dp *dp, u32 cmd, u32 addr,
> + u8 *buf, u32 bytes, u8 *reply)
> +{
[ ... ]
> + 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;
> +}
[Severity: Low]
Should the CIX_DPTX_AUX_REPLY_DATA read loop be inside the ACK check? When a
sink responds to a read with NACK or DEFER, the loop still runs and overwrites
buf with stale or empty FIFO reads before drm_dp_aux retries.
> +
> +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]
In addition to cix_dp_aux_cmd_submit() reading from the empty
CIX_DPTX_AUX_REPLY_DATA FIFO when the sink replies with NACK or DEFER, does
cix_dp_aux_transfer() keep submitting later 16-byte chunks after a non-ACK
reply? cix_dp_aux_cmd_submit() returns 0 on NACK and DEFER, so a multi-chunk
transfer continues looping and overwrites msg->reply instead of stopping and
returning to the caller.
[ ... ]
> +static int cix_dp_set_misc(struct cix_dp *dp,
> + struct cix_dp_panel *dp_panel)
> +{
> + struct cix_dp_config *config;
> +
> + if (!dp_panel || !dp_panel->connector)
> + return -EINVAL;
> +
> + config = &dp_panel->connector->config;
> +
> + config->misc0 &= ~CIX_DPTX_STREAM_MISC0_SYNC_LOCK;
> + config->misc1 &= ~CIX_DPTX_VSC_COLORIMETRY_EN;
> + config->override = 0;
> +
> + if (dp->caps.vsc_supported &&
> + cix_dp_needs_vsc_sdp(config->format, config->colorspace)) {
> + config->misc1 |= CIX_DPTX_VSC_COLORIMETRY_EN;
> +
> + switch (config->bpc) {
> + case 6:
> + config->override = CIX_DPTX_STREAM_OVERRIDE_BPC_6;
> + break;
[ ... ]
> + default:
> + config->override = CIX_DPTX_STREAM_OVERRIDE_BPC_8;
> + break;
> + }
> +
> + if (config->format == CIX_DP_FORMAT_YCBCR420)
> + config->override |= CIX_DPTX_STREAM_OVERRIDE_YCbCR_420;
> +
> + config->override |= CIX_DPTX_STREAM_OVERRIDE_ENABLE;
> + config->dynamic_range = DP_DYNAMIC_RANGE_CTA;
> + config->content_type = DP_CONTENT_TYPE_NOT_DEFINED;
> +
> + goto end;
> + }
[ ... ]
> +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;
> + }
[Severity: High]
Does dptx_setup_vsc_sdp() program the wrong VSC SDP payload for YCbCr 4:2:2,
RGB, and 6 bpc?
CIX_DP_FORMAT_YCBCR422 sets sdp->db[16] to 0x1 << 4 (YCbCr 4:4:4) instead of
0x2 << 4 (YCbCr 4:2:2). In the default (RGB) branch, sdp->db[16] is not
cleared before the colorspace switch ORs in the colorimetry bits, leaving
stale format and colorimetry bits from earlier modesets in
conn->sdp[CIX_VSC_SDP]. Further down in dptx_setup_vsc_sdp(), switch
(config->bpc) has no case 6, so 6 bpc RGB falls into default and encodes
0x1 (8 bpc) instead of 0x0 in sdp->db[17].
In cix_dp_set_misc() above, the VSC path only sets the pixel format bits in
config->override for CIX_DP_FORMAT_YCBCR420 (leaving YCbCr 4:2:2 and 4:4:4
as 0/RGB in the override register) and overwrites config->content_type with
DP_CONTENT_TYPE_NOT_DEFINED instead of using conn_state->content_type. Also,
cix_dp_pick_format_bpc() and cix_dp_connector_mode_valid() select
DRM_OUTPUT_COLOR_FORMAT_YCBCR420 without checking dp->caps.vsc_supported;
when vsc_supported is false, cix_dp_set_misc() falls through and returns
-EINVAL (which cix_dp_panel_hw_cfg() ignores), leaving MISC0, MISC1,
OVERRIDE, and the VSC SDP unconfigured while the RCSU adapter is set to
YCbCr 4:2:0.
[ ... ]
> +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]
Does enabling the bridge when attached with DRM_BRIDGE_ATTACH_NO_CONNECTOR
cause a NULL pointer dereference on dp_panel->connector?
When DRM_BRIDGE_ATTACH_NO_CONNECTOR is passed to cix_dp_bridge_attach(), it
returns 0 without calling cix_dp_connector_create(), leaving
dp->dp_panel.connector NULL. On modeset enable, cix_dp_panel_hw_cfg()
ignores the -EINVAL return from cix_dp_set_misc() and calls
cix_dp_panel_setup_colorimetry_sdp(), which dereferences
conn->config.override, followed by cix_dp_panel_hw_cfg() dereferencing
conn->config.bpp. In addition, cix_dp_hpd_event_work_func() exits early when
!dp->dp_panel.connector and cix_dp_bridge_funcs does not implement the bridge
detect, EDID, or HPD callbacks needed without an internal connector.
[ ... ]
> +/*
> + * Pure probe: report the current HPD level without touching the session state
> + * or the hardware. This runs on every DRM probe, poll and hotplug, so
> + * DPTX_STATE_CONNECTED is advanced from the HPD event work and the encoder
> + * prepare path instead.
> + *
> + * The host is initialized by probe, resume and cix_dp_prepare(). Bail out
> + * loudly if we are ever called before that: cix_dp_read() is a plain readl()
> + * and would hang on a gated APB clock.
> + */
> +static int cix_dp_detect(struct cix_dp *dp)
> +{
> + int ret = 0;
> +
> + mutex_lock(&dp->session_lock);
> +
> + if (dp->state & DPTX_STATE_SUSPENDED) {
> + ret = -EBUSY;
> + } else if (!(dp->state & DPTX_STATE_INITIALIZED)) {
> + dev_warn_once(dp->dev, "detect with uninitialized host\n");
> + ret = -ENODEV;
> + }
> +
> + mutex_unlock(&dp->session_lock);
> +
> + if (ret)
> + return ret;
> +
> + return cix_dp_get_hpd_state(dp) ? 0 : -ENODEV;
> +}
[Severity: High]
Can MMIO registers be read while dp->apb_clk is gated, hanging the bus?
As noted in the comment above cix_dp_detect(), reading DPTX registers while
the host is uninitialized and apb_clk is gated hangs the bus:
- cix_dp_aux_transfer() calls cix_dp_get_hpd_state() and
cix_dp_aux_cmd_submit() without acquiring dp->session_lock or checking
DPTX_STATE_INITIALIZED / DPTX_STATE_SUSPENDED, so /dev/drm_dp_aux* or I2C
transfers during suspend read unclocked MMIO registers.
- cix_dp_hpd_irq_work_func() and cix_dp_hpd_event_work_func() call
cix_dp_get_hpd_state() before acquiring dp->session_lock and checking
DPTX_STATE_INITIALIZED.
- cix_dp_detect() drops dp->session_lock before calling
cix_dp_get_hpd_state(), and cix_dp_prepare() drops and re-acquires
dp->session_lock around cix_dp_handle_connect() without rechecking
DPTX_STATE_INITIALIZED, allowing a concurrent cix_dp_pm_prepare() to gate
apb_clk in between.
[ ... ]
> +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]
Won't drm_dp_dpcd_readb() in cix_dp_replay_deferred_hotplug() always fail
with -ENODEV?
cix_dp_replay_deferred_hotplug() only reaches drm_dp_dpcd_readb() when
cix_dp_get_hpd_state(dp) is false, and it does not set
dp->force_hpd = connector_status_connected until after the read succeeds.
However, drm_dp_dpcd_readb() calls cix_dp_aux_transfer(), which immediately
checks if (!cix_dp_get_hpd_state(dp)) and returns -ENODEV, so a USB-C display
plugged in before bridge attach is never detected.
> +
> +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]
Can sink DPCD capabilities remain unread across boot, resume, and hotplug,
and can concurrent connector probes suppress hotplug events?
If a native DP/eDP sink is already connected at boot, the HPD interrupt fires
in cix_dp_init() before cix_dp_bridge_attach() sets dp->dp_panel.connector,
so cix_dp_hpd_event_work_func() returns early; later,
cix_dp_replay_deferred_hotplug() also returns early because
cix_dp_get_hpd_state(dp) is true. Likewise, cix_dp_pm_prepare() clears
DPTX_STATE_CONNECTED on suspend without cix_dp_pm_complete() re-probing DPCD
on resume. Because cix_dp_connector_detect() only checks HPD level without
calling cix_dp_handle_connect(), dp->dpcd, dp->downstream_ports, dp->caps,
and dp->link_config remain unpopulated during mode validation, atomic_check,
and cix_dp_bridge_mode_set() (which runs before encoder_enable() ->
cix_dp_prepare() calls cix_dp_handle_connect()).
Even when cix_dp_hpd_event_work_func() runs, it calls
drm_helper_probe_detect() before cix_dp_handle_connect(), so
cix_dp_connector_detect() passes stale dp->dpcd and dp->downstream_ports to
drm_dp_set_subconnector_property(), and any concurrent DRM connector probe
updates dp->status before old_status = dp->status is read:
CPU0 (kworker) CPU1 (userspace probe)
cix_dp_hpd_event_work_func()
msleep(100) waiting for PHY
cix_dp_connector_detect()
dp->status = connected
old_status = dp->status (connected)
drm_helper_probe_detect()
if (old_status != dp->status) <-- false, HPD event dropped
Finally, cix_dp_handle_connect() only clears DPTX_STATE_CONNECTED when
cix_dp_panel_read_sink_caps() returns -ETIMEDOUT or -ENOTCONN; if it fails
with -EIO, -EPROTO, -EBUSY, or -ENODEV, DPTX_STATE_CONNECTED stays set and
prevents later cix_dp_handle_connect() calls from reading the DPCD.
[ ... ]
> +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;
> +}
[Severity: Medium]
If the display controller resumes and calls cix_dp_bridge_atomic_enable() ->
cix_dp_prepare() before cix_dptx_pm_resume() -> cix_dp_pm_complete() clears
DPTX_STATE_SUSPENDED, will the display stay blank?
cix_dp_prepare() handles !(dp->state & DPTX_STATE_INITIALIZED) by calling
cix_dp_host_init(), and then calls cix_dp_handle_connect() to restore
DPTX_STATE_CONNECTED (which cix_dp_pm_prepare() cleared). However,
cix_dp_handle_connect() bails out immediately when DPTX_STATE_SUSPENDED is
still set, leaving DPTX_STATE_CONNECTED clear so cix_dp_prepare() skips
cix_dp_core_on() and cix_dp_enable() skips enabling the stream.
[ ... ]
> +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: Medium]
Does a failure in cix_dp_enable() leak PHY init/power references and leave
dp->state inconsistent?
cix_dp_prepare() -> cix_dp_core_on() 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, cix_dp_encoder_enable() does not roll back via
cix_dp_core_off(), and a later disable also skips cix_dp_core_off() because
cix_dp_unprepare() returns early when !(dp->state & DPTX_STATE_ENABLED). On
the next cix_dp_prepare() call, the else if (dp->state &
DPTX_STATE_INIT_TRAIN) branch calls reset_dp_and_reinit(), which resets the
controller and calls phy_init() again without a matching phy_exit(), while
cix_dp_core_on() sees DPTX_STATE_READY still set and returns 0 without
retraining the link.
Similarly, in cix_dp_pm_complete(), if cix_dp_host_init() fails on resume,
enable_irq(dp->irq) is skipped and DPTX_STATE_SUSPENDED stays set, while the
next cix_dp_pm_prepare() or cix_dp_deinit() calls disable_irq(dp->irq)
unconditionally.
[ ... ]
> +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: Medium]
Can HPD delayed works be re-queued while cix_dp_deinit(),
cix_dp_bridge_detach(), or cix_dp_pm_prepare() is tearing down the driver?
In cix_dp_deinit(), cancel_delayed_work_sync() runs before
disable_irq(dp->irq):
CPU0 CPU1 (irq/kworker)
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()
mutex_destroy(&dp->session_lock)
(devm frees dp)
cix_dp_hpd_event_work_func()
<-- use-after-free / gated APB read
Two related races exist in the other teardown paths:
- cix_dp_bridge_detach() cancels hpd_irq_work before sw_hpd_irq_work (even
though cix_dp_sw_hpd_irq_work_func() queues hpd_irq_work) and leaves
dp->irq enabled and dp->dp_panel.connector non-NULL while calling
drm_connector_cleanup(&dp->connector.base).
- cix_dp_pm_prepare() cancels the delayed works before setting
DPTX_STATE_SUSPENDED, while cix_dp_oob_hotplug_event() checks
DPTX_STATE_SUSPENDED unlocked and can schedule hpd_event_work and
sw_hpd_irq_work after the cancel calls.
[ ... ]
> +static void cix_dptx_remove(struct platform_device *pdev)
> +{
> + struct cix_dp *dp = dev_get_drvdata(&pdev->dev);
> +
> + if (!dp)
> + return;
> +
> + drm_bridge_remove(&dp->bridge);
> + cix_dp_deinit(dp);
> +}
[ ... ]
> +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]
What happens to active streams and PHY power if cix_dptx_shutdown() or
cix_dptx_remove() runs while a display stream is enabled?
In cix_dptx_shutdown(), zeroing dp->active_stream_cnt without calling
cix_dp_ctrl_stream_off() or cix_dp_core_off() causes cix_dp_pm_prepare() ->
cix_dp_host_deinit() to call phy_exit(dp->phy) while the PHY is still powered
on (and with the second phy_init() reference from cix_dp_core_on() and
stream_clk still active). In cix_dptx_remove(), dp->active_stream_cnt is not
cleared and streams are not stopped, so cix_dp_host_deinit() returns early at
if (dp->active_stream_cnt) and leaves the transmitter, clocks, and PHY
enabled.
> 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
[ ... ]
> +static int cix_dp_cal_bpc(struct cix_dp *dp,
> + struct drm_connector_state *connector_state, int bpc)
> +{
> + struct drm_display_info *info =
> + &connector_state->connector->display_info;
> +
> + if (connector_state->colorspace >= DRM_MODE_COLORIMETRY_BT2020_CYCC &&
> + connector_state->colorspace <= DRM_MODE_COLORIMETRY_BT2020_YCC)
> + bpc = 10;
> +
> + if (info->bpc && bpc > info->bpc)
> + bpc = info->bpc;
> +
> + if (connector_state->max_requested_bpc &&
> + bpc > connector_state->max_requested_bpc)
> + bpc = connector_state->max_requested_bpc;
> +
> + return bpc;
> +}
[Severity: Medium]
When a BT.2020 colorspace is set, doesn't overwriting bpc = 10 in
cix_dp_cal_bpc() prevent cix_dp_pick_format_bpc() from stepping down to 8 or
6 bpc when link bandwidth cannot fit 10 bpc? Each fallback call to
compute_available_clock_rate() with curr_bpc = 8 or 6 gets forced back to 10.
Also, in cix_dp_pick_config(), passing max(info->bpc, 6) as suggest_bpc
selects 6 bpc instead of 8 bpc whenever an EDID leaves info->bpc as 0.
[ ... ]
> +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;
> + }
[Severity: Medium]
Will the plain 64-bit division in (int64_t)mode->clock *
adjusted_mode->htotal / mode->htotal fail to link with an undefined reference
to __divdi3 / __aeabi_ldivmod on 32-bit architectures? DRM_CIX_DP does not
depend on 64BIT, so this needs div_s64() or mul_u32_u32() with div_u64().
> +
> + /* The sync polarities are inverted with respect to the source mode. */
> + 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 &&\n+ drm_mode_is_420_only(info, adjusted_mode)))
> + continue;
[Severity: High]
Does inverting the sync polarity flags on adjusted_mode in
cix_dp_adjust_mode() break drm_mode_is_420_only(info, adjusted_mode) and
YCbCr 4:2:0 format selection?
cix_dp_bridge_atomic_check() calls cix_dp_adjust_mode() before
cix_dp_pick_config(). Because cix_dp_adjust_mode() flips
DRM_MODE_FLAG_PVSYNC and DRM_MODE_FLAG_PHSYNC on adjusted_mode (and may alter
htotal and clock), drm_match_cea_mode(adjusted_mode) inside
drm_mode_is_420_only() fails to match the CEA mode table and returns 0.
Even when a mode is 4:2:0-only, cix_dp_pick_format_bpc() iterates fmt_prio[]
from RGB444 and YCBCR422 without checking drm_mode_is_420_only() first, so any
4:2:0-only mode whose clock fits on the link at RGB or YCbCr 4:2:2 picks a
non-4:2:0 format. Conversely, when info->color_formats has
DRM_OUTPUT_COLOR_FORMAT_YCBCR420 set, fmt_prio[4] can pick YCBCR420 on modes
where the sink does not support 4:2:0, and without checking that
adjusted_mode->clock <= CIX_DP_MAX_FREQ (since YCbCr 4:2:0 forces 1 pixel per
cycle).
[ ... ]
> +static void cix_dp_bridge_mode_set(struct cix_dp *dp,
> + struct drm_crtc_state *crtc_state,
> + struct drm_connector_state *conn_state)
> +{
[ ... ]
> + 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;
> + 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);
[ ... ]
> +}
[ ... ]
> +static void
> +cix_dp_bridge_atomic_enable(struct drm_bridge *bridge,
> + struct drm_atomic_commit *state)
> +{
[ ... ]
> + cix_dp_bridge_mode_set(dp, crtc_state, conn_state);
> +
> + ret = dp->ops->encoder_enable(dp, dp->dp_panel.source_id, true);
> + if (ret)
> + dev_err(dp->dev, "failed to enable DP output: %d\n", ret);
> +}
[Severity: Medium]
Should cix_dp_bridge_mode_set() run under dp->session_lock and after
cix_dp_prepare() has read the sink's DPCD?
cix_dp_bridge_atomic_enable() calls cix_dp_bridge_mode_set() unlocked before
dp->ops->encoder_enable() -> cix_dp_prepare(). This mutates
dp->connector.config, dp->pixel_per_cycle, and dp->mode without holding
dp->session_lock (racing with cix_dp_hpd_irq_work_func() ->
cix_dp_train_loop()), and computes link_rate and lane_cnt from
dp->link_config before cix_dp_prepare() -> cix_dp_handle_connect() /
cix_dp_core_on() updates dp->link_config from the sink's DPCD.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261109092623.102291-1-Luo.Liu@cixtech.com?part=2
next prev parent reply other threads:[~2026-11-09 20:30 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-09 9:26 [PATCH 0/2] drm/cix: add Sky1 DisplayPort transmitter Luo Liu
2026-11-09 9:26 ` [PATCH 1/2] dt-bindings: display: add CIX " Luo Liu
2026-11-09 20:04 ` sashiko-bot
2026-11-09 9:26 ` [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver Luo Liu
2026-11-09 20:30 ` sashiko-bot [this message]
-- strict thread matches above, loose matches on Subject: below --
2026-11-09 10:30 [PATCH 0/2] drm/cix: add Sky1 DisplayPort transmitter Luo Liu
2026-11-09 10:30 ` [PATCH 2/2] drm/cix: add Sky1 DisplayPort transmitter driver Luo Liu
2026-11-09 10:55 ` 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-205583@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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.