From: sashiko-bot@kernel.org
To: "Chaoyi Chen" <kernel@airkyi.com>
Cc: dri-devel@lists.freedesktop.org, linux-phy@lists.infradead.org,
vkoul@kernel.org, neil.armstrong@linaro.org, olteanv@gmail.com
Subject: Re: [PATCH v8 7/7] drm/rockchip: cdn-dp: Add multiple bridges to support PHY port selection
Date: Tue, 20 Oct 2026 09:19:41 +0000 [thread overview]
Message-ID: <sashiko-outbox-178716@kernel.org> (raw)
In-Reply-To: <20261020072816.180-8-kernel@airkyi.com>
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] drm/rockchip: cdn-dp: unguarded cdn_dp_config_video() in cdn_dp_switch_port()
- [High] drm/rockchip: cdn-dp: missing PHY switch in cdn_dp_bridge_atomic_enable()
- [High] drm/rockchip: cdn-dp: encoder use-after-free on cdn_dp_bind() failure
- [High] drm/rockchip: cdn-dp: missing per-bridge check in cdn_dp_bridge_detect()
- [High] drm/rockchip: cdn-dp: ignored bridge HPD state in cdn_dp_connected_port()
- [Medium] drm/rockchip: cdn-dp: race on dp->active_port in cdn_dp_connected_port()
--
commit ecc80bdc2aa1d377f4a7cb763c9aafe0d00f7b9f
Author: Chaoyi Chen <chaoyi.chen@rock-chips.com>
drm/rockchip: cdn-dp: Add multiple bridges to support PHY port selection
This commit adds support for registering multiple DRM bridges and encoders
in the Rockchip CDN-DP driver so that users can select between multiple PHY
ports using the DRM AUX HPD bridge infrastructure.
> diff --git a/drivers/gpu/drm/rockchip/cdn-dp-core.c b/drivers/gpu/drm/rockchip/cdn-dp-core.c
> index 1c1c1116b5ebe..4067802904250 100644
> --- a/drivers/gpu/drm/rockchip/cdn-dp-core.c
> +++ b/drivers/gpu/drm/rockchip/cdn-dp-core.c
[ ... ]
> @@ -191,14 +192,27 @@ static int cdn_dp_get_sink_count(struct cdn_dp_device *dp, u8 *sink_count)
> static struct cdn_dp_port *cdn_dp_connected_port(struct cdn_dp_device *dp)
> {
> struct cdn_dp_port *port;
> - int i, lanes;
> + int i, lanes[MAX_PHY];
>
> for (i = 0; i < dp->ports; i++) {
> port = dp->port[i];
> - lanes = cdn_dp_get_port_lanes(port);
> - if (lanes)
> + lanes[i] = cdn_dp_get_port_lanes(port);
> + if (!dp->next_bridge_valid && lanes[i])
> return port;
> }
> +
> + if (dp->next_bridge_valid) {
> + /* If more than one port is available, pick the last active port */
> + if (dp->active_port >= 0 && lanes[dp->active_port])
> + return dp->port[dp->active_port];
[Severity: Medium]
Can dp->active_port race here when cdn_dp_connected_port() is called
without holding dp->lock?
In cdn_dp_bridge_atomic_disable(), dp->lock is released before calling
cdn_dp_connected_port(dp):
drivers/gpu/drm/rockchip/cdn-dp-core.c:cdn_dp_bridge_atomic_disable() {
...
mutex_unlock(&dp->lock);
...
if (!dp->connected && cdn_dp_connected_port(dp))
schedule_work(&dp->event_work);
}
Concurrently, cdn_dp_pd_event_work(), cdn_dp_bridge_edid_read() ->
cdn_dp_switch_port(), or cdn_dp_bridge_atomic_enable() can update
dp->active_port under dp->lock, including setting dp->active_port = -1 in
cdn_dp_disable_phy().
If dp->active_port is set to -1 right after the dp->active_port >= 0 check,
won't lanes[dp->active_port] or dp->port[dp->active_port] read out of bounds
at index -1?
> +
> + /* If the last active port is not available, pick an available port in order */
> + for (i = 0; i < dp->bridge_count; i++) {
> + if (lanes[i])
> + return dp->port[i];
> + }
> + }
> +
> return NULL;
> }
[Severity: High]
When dp->next_bridge_valid is true, cdn_dp_bridge_hpd_notify() records the
per-port HPD state in dp_bridge->connected, but cdn_dp_connected_port(),
cdn_dp_enable(), and cdn_dp_check_sink_connection() never inspect
dp->bridge_list[i]->connected.
Instead, cdn_dp_connected_port() checks cdn_dp_get_port_lanes(port), which
calls phy_get_bus_width(port->phy) when port->extcon is NULL. If the PHY bus
width remains non-zero while HPD is deasserted (for example, with a Type-C
dock or adapter in DP altmode with no monitor plugged in, or before altmode
teardown completes), won't cdn_dp_connected_port() treat the disconnected
port as connected?
Even if lanes[0] becomes 0 when active port 0 is unplugged while port 1
remains connected (with dp->active == true and dp->active_port == 0),
cdn_dp_pd_event_work() sees cdn_dp_connected_port(dp) return port 1 and
takes the else if (!cdn_dp_check_sink_connection(dp)) branch:
drivers/gpu/drm/rockchip/cdn-dp-core.c:cdn_dp_pd_event_work() {
...
if (!cdn_dp_connected_port(dp)) {
...
} else if (!dp->active) {
...
} else if (!cdn_dp_check_sink_connection(dp)) {
DRM_DEV_INFO(dp->dev, "Connected without sink; assert hpd\n");
dp->connected = false;
...
}
Because cdn_dp_check_sink_connection() checks dp->port[dp->active_port]
(the unplugged port 0) and has no !dp_bridge->connected early exit when
port->extcon is NULL, won't it poll DPCD for CDN_DPCD_TIMEOUT_MS (5 seconds)
and then set dp->connected = false instead of switching to port 1, leaving
port 1 unusable when no bridge was enabled in KMS to trigger
cdn_dp_bridge_atomic_disable()?
[ ... ]
> @@ -550,6 +598,50 @@ static bool cdn_dp_check_link_status(struct cdn_dp_device *dp)
> return drm_dp_channel_eq_ok(link_status, min(port->lanes, sink_lanes));
> }
>
> +static int cdn_dp_switch_port(struct cdn_dp_device *dp, struct cdn_dp_port *prev_port,
> + struct cdn_dp_port *port)
> +{
> + int ret;
> +
> + ret = cdn_dp_disable_phy(dp, prev_port);
> + if (ret)
> + goto out;
> + ret = cdn_dp_enable_phy(dp, port);
> + if (ret)
> + goto out;
> +
> + ret = cdn_dp_get_sink_capability(dp);
> + if (ret) {
> + cdn_dp_disable_phy(dp, port);
> + goto out;
> + }
> +
> + dp->lanes = port->lanes;
> +
> + if (!cdn_dp_check_link_status(dp)) {
> + dev_info(dp->dev, "Connected with sink; re-train link\n");
> +
> + ret = cdn_dp_train_link(dp);
> + if (ret) {
> + dev_err(dp->dev, "Training link failed: %d\n", ret);
> + goto out;
> + }
> +
> + ret = cdn_dp_set_video_status(dp, CONTROL_VIDEO_IDLE);
> + if (ret) {
> + dev_err(dp->dev, "Failed to idle video %d\n", ret);
> + goto out;
> + }
> +
> + ret = cdn_dp_config_video(dp);
> + if (ret)
> + dev_err(dp->dev, "Failed to configure video: %d\n", ret);
> + }
> +
> +out:
> + return ret;
> +}
[Severity: High]
Does cdn_dp_switch_port() always fail with -EINVAL when called from
cdn_dp_bridge_edid_read() before the first modeset commit?
cdn_dp_bridge_edid_read() only calls cdn_dp_switch_port() when no bridge is
currently enabled. Before the first modeset (cdn_dp_bridge_mode_set() and
cdn_dp_display_info_update()), dp->mode.clock and dp->video_info.color_depth
are both 0.
When switching to the new port, cdn_dp_check_link_status(dp) returns false
and cdn_dp_switch_port() unconditionally calls cdn_dp_config_video(dp)
(unlike cdn_dp_pd_event_work(), which checks mode->clock first). In
cdn_dp_config_video():
drivers/gpu/drm/rockchip/cdn-dp-reg.c:cdn_dp_config_video() {
...
do {
tu_size_reg += 2;
symbol = (u64)tu_size_reg * mode->clock * bit_per_pix;
do_div(symbol, dp->max_lanes * link_rate * 8);
rem = do_div(symbol, 1000);
if (tu_size_reg > 64) {
ret = -EINVAL;
DRM_DEV_ERROR(dp->dev,
"tu error, clk:%d, lanes:%d, rate:%d\n",
mode->clock, dp->max_lanes, link_rate);
goto err_config_video;
}
} while ((symbol <= 1) || (tu_size_reg - symbol < 4) ||
(rem > 850) || (rem < 100));
}
With mode->clock == 0, symbol is 0 on every iteration until tu_size_reg > 64,
returning -EINVAL. In cdn_dp_bridge_edid_read(), that error causes a jump to
get_cache, skipping drm_edid_read_custom() and returning NULL when
edid_cache is empty.
Also, if cdn_dp_enable_phy(dp, port) or cdn_dp_get_sink_capability(dp) fails
after cdn_dp_disable_phy(dp, prev_port) has already disabled prev_port,
prev_port is not re-enabled. Doesn't that leave all ports with
phy_enabled == false and dp->active_port == -1 while dp->active remains true,
so subsequent calls to cdn_dp_bridge_edid_read() find prev_port == NULL and
attempt to read EDID with all PHYs powered off?
[ ... ]
> @@ -599,6 +692,9 @@ static void cdn_dp_bridge_atomic_enable(struct drm_bridge *bridge, struct drm_at
>
> mutex_lock(&dp->lock);
>
> + if (dp->next_bridge_valid)
> + dp->active_port = dp_bridge->id;
> +
> ret = cdn_dp_enable(dp);
> if (ret) {
> DRM_DEV_ERROR(dp->dev, "Failed to enable bridge %d\n",
> @@ -631,6 +727,7 @@ static void cdn_dp_bridge_atomic_enable(struct drm_bridge *bridge, struct drm_at
> goto out;
> }
>
> + dp_bridge->enabled = true;
> out:
> mutex_unlock(&dp->lock);
> }
[Severity: High]
How does cdn_dp_bridge_atomic_enable() switch PHYs when dp->active is
already true on the other port?
When a sink is connected, cdn_dp_pd_event_work() calls cdn_dp_enable(dp),
which enables one PHY port and sets dp->active = true. Similarly, probing
connectors via cdn_dp_bridge_edid_read() can leave the other port's PHY
enabled while dp->active is true.
When cdn_dp_bridge_atomic_enable() then runs for dp_bridge, it sets
dp->active_port = dp_bridge->id and calls cdn_dp_enable(dp), which returns
immediately when dp->active is true:
drivers/gpu/drm/rockchip/cdn-dp-core.c:cdn_dp_enable() {
...
if (dp->active)
return 0;
...
}
Doesn't this leave the previously enabled PHY powered on and
dp->port[dp_bridge->id]->phy_enabled false, causing link training and video
output in cdn_dp_bridge_atomic_enable() to run against the wrong PHY?
In addition, since each bridge registers a separate drm_encoder with the
same possible_crtcs and there is no atomic_check preventing both bridges
from being enabled at the same time, what happens if userspace enables both
bridges and then disables one in cdn_dp_bridge_atomic_disable(), which calls
cdn_dp_disable(dp) and shuts down the entire controller?
[ ... ]
> @@ -947,10 +1062,12 @@ static void cdn_dp_pd_event_work(struct work_struct *work)
> }
>
> out:
> + notify = last_connected != dp->connected;
> mutex_unlock(&dp->lock);
> - drm_bridge_hpd_notify(&dp->bridge,
> - dp->connected ? connector_status_connected
> - : connector_status_disconnected);
> + for (i = 0; i < dp->bridge_count; i++) {
> + if (notify)
> + drm_kms_helper_connector_hotplug_event(dp->bridge_list[i]->connector);
> + }
> }
[Severity: High]
Should cdn_dp_bridge_detect() also check dp_bridge->connected, similar to
cdn_dp_bridge_mode_valid()?
Currently cdn_dp_bridge_detect() only checks the controller-wide
dp->connected flag:
drivers/gpu/drm/rockchip/cdn-dp-core.c:cdn_dp_bridge_detect() {
struct cdn_dp_device *dp = bridge_to_dp(bridge);
enum drm_connector_status status = connector_status_disconnected;
mutex_lock(&dp->lock);
if (dp->connected)
status = connector_status_connected;
mutex_unlock(&dp->lock);
return status;
}
Because each bridge sets DRM_BRIDGE_OP_DETECT, drm_bridge_connector_init()
selects cdn_dp_bridge as bridge_detect for each connector. When port 0 is
connected (dp->connected == true) and port 1 is disconnected
(bridge_list[1]->connected == false), cdn_dp_pd_event_work() calls
drm_kms_helper_connector_hotplug_event() on all connectors.
When drm_bridge_connector_detect_ctx() probes the disconnected port 1
connector:
drivers/gpu/drm/display/drm_bridge_connector.c:drm_bridge_connector_detect_ctx() {
...
if (detect) {
status = detect->funcs->detect(detect, connector);
...
drm_bridge_connector_hpd_notify(connector, status);
}
...
}
cdn_dp_bridge_detect() returns connector_status_connected for port 1, and
drm_bridge_connector_hpd_notify() immediately calls
cdn_dp_bridge_hpd_notify() on bridge_list[1] with
connector_status_connected, overwriting bridge_list[1]->connected = true and
causing port 1 to attempt an EDID read (switching PHYs if port 0 is not yet
enabled).
Conversely, when a port first signals HPD connect via
drm_bridge_connector_handle_hpd(), cdn_dp_bridge_hpd_notify() sets
dp_bridge->connected = true and schedules dp->event_work, while
drm_bridge_connector_handle_hpd() immediately triggers connector probing. If
drm_bridge_connector_detect_ctx() runs before dp->event_work sets
dp->connected = true, won't cdn_dp_bridge_detect() return
connector_status_disconnected and overwrite dp_bridge->connected back to
false via drm_bridge_connector_hpd_notify()?
>
> static int cdn_dp_pd_event(struct notifier_block *nb,
> @@ -970,28 +1087,16 @@ static int cdn_dp_pd_event(struct notifier_block *nb,
> return NOTIFY_DONE;
> }
>
> -static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
> +static int cdn_bridge_add(struct device *dev,
> + struct drm_bridge *bridge,
> + struct drm_bridge *next_bridge,
> + struct drm_encoder *encoder)
> {
> struct cdn_dp_device *dp = dev_get_drvdata(dev);
> - struct drm_encoder *encoder;
> + struct drm_device *drm_dev = dp->drm_dev;
> + struct drm_bridge *last_bridge __free(drm_bridge_put) = NULL;
> struct drm_connector *connector;
> - struct cdn_dp_port *port;
> - struct drm_device *drm_dev = data;
> - int ret, i;
> -
> - ret = cdn_dp_parse_dt(dp);
> - if (ret < 0)
> - return ret;
> -
> - dp->drm_dev = drm_dev;
> - dp->connected = false;
> - dp->active = false;
> - dp->active_port = -1;
> - dp->fw_loaded = false;
> -
> - INIT_WORK(&dp->event_work, cdn_dp_pd_event_work);
> -
> - encoder = &dp->encoder.encoder;
> + int ret;
>
> encoder->possible_crtcs = drm_of_find_possible_crtcs(drm_dev,
> dev->of_node);
[ ... ]
> @@ -1033,6 +1147,108 @@ static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
> return ret;
> }
>
> + if (last_bridge) {
> + fwnode_handle_put(connector->fwnode);
> + connector->fwnode = fwnode_handle_get(of_fwnode_handle(last_bridge->of_node));
> + }
> +
> + bridge_to_dp_bridge(bridge)->connector = connector;
> +
> + return 0;
> +}
[ ... ]
> +static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
> +{
> + struct cdn_dp_device *dp = dev_get_drvdata(dev);
> + struct drm_bridge *bridge, *next_bridge;
> + struct drm_encoder *encoder;
> + struct cdn_dp_port *port;
> + struct drm_device *drm_dev = data;
> + struct cdn_dp_bridge *dp_bridge;
> + int ret, i;
> +
> + ret = cdn_dp_parse_dt(dp);
> + if (ret < 0)
> + return ret;
> +
> + ret = cdn_dp_parse_next_bridge_dt(dp);
> + if (ret)
> + goto out;
> +
> + dp->drm_dev = drm_dev;
> + dp->connected = false;
> + dp->active = false;
> + dp->active_port = -1;
> + dp->fw_loaded = false;
> +
> + for (i = 0; i < dp->bridge_count; i++) {
> + dp_bridge = devm_drm_bridge_alloc(dev, struct cdn_dp_bridge, bridge,
> + &cdn_dp_bridge_funcs);
> + if (IS_ERR(dp_bridge)) {
> + ret = PTR_ERR(dp_bridge);
> + goto out;
> + }
> + dp_bridge->id = i;
> + dp_bridge->parent = dp;
> + if (!dp->next_bridge_valid)
> + dp_bridge->connected = true;
> + dp->bridge_list[i] = dp_bridge;
> + }
> +
> + for (i = 0; i < dp->bridge_count; i++) {
> + encoder = &dp->bridge_list[i]->encoder.encoder;
> + bridge = &dp->bridge_list[i]->bridge;
> + next_bridge = dp->next_bridge_list[i];
> + ret = cdn_bridge_add(dev, bridge, next_bridge, encoder);
> + if (ret)
> + goto out;
> + }
> +
> + INIT_WORK(&dp->event_work, cdn_dp_pd_event_work);
> +
> for (i = 0; i < dp->ports; i++) {
> port = dp->port[i];
>
> @@ -1046,7 +1262,7 @@ static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
> if (ret) {
> DRM_DEV_ERROR(dev,
> "register EXTCON_DISP_DP notifier err\n");
> - return ret;
> + goto out;
> }
> }
>
> @@ -1055,15 +1271,31 @@ static int cdn_dp_bind(struct device *dev, struct device *master, void *data)
> schedule_work(&dp->event_work);
>
> return 0;
> +
> +out:
> + for (i = 0; i < MAX_PHY; i++)
> + drm_bridge_clear_and_put(&dp->next_bridge_list[i]);
> +
> + return ret;
> }
[Severity: High]
Can a failure in cdn_dp_bind() cause a use-after-free on encoder?
In this patch, struct rockchip_encoder encoder is embedded inside struct
cdn_dp_bridge, which is allocated with devm_drm_bridge_alloc() inside
cdn_dp_bind() (under component_bind()'s devres group).
In cdn_bridge_add(), drm_encoder_init() adds &dp_bridge->encoder.encoder to
drm_dev->mode_config.encoder_list. If a subsequent step in cdn_bridge_add()
or cdn_dp_bind() fails (for example, drm_bridge_attach() on next_bridge,
drm_bridge_connector_init(), the second iteration of cdn_bridge_add(), or
devm_extcon_register_notifier()), the error path at out: does not destroy
any already-initialized encoders.
When component_bind() then releases its devres group, the devm reference on
dp_bridge is dropped, freeing dp_bridge (either immediately if
drm_bridge_attach() on dp_bridge->bridge did not run, or during
drm_encoder_cleanup() -> drm_bridge_detach() when
drm_mode_config_cleanup() unwinds) while drm_encoder_cleanup() still
accesses the embedded encoder. Should the error path clean up any
initialized encoders (or use drmm_encoder_alloc() / separate the encoder
allocation from the bridge container)?
>
> static void cdn_dp_unbind(struct device *dev, struct device *master, void *data)
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261020072816.180-1-kernel@airkyi.com?part=7
--
linux-phy mailing list
linux-phy@lists.infradead.org
https://lists.infradead.org/mailman/listinfo/linux-phy
prev parent reply other threads:[~2026-10-20 9:19 UTC|newest]
Thread overview: 13+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-20 7:28 [PATCH v8 0/7] drm/bridge: Implement generic USB Type-C DP HPD bridge Chaoyi Chen
2026-10-20 7:28 ` [PATCH v8 1/7] drm/bridge: aux-hpd-bridge: Add drm_dev_has_dp_hpd_bridge() Chaoyi Chen
2026-10-20 7:28 ` [PATCH v8 2/7] drm/bridge: Implement generic USB Type-C DP HPD bridge Chaoyi Chen
2026-10-20 8:17 ` sashiko-bot
2026-10-20 7:28 ` [PATCH v8 3/7] drm/display: Add soft depend for aux-hpd-typec-dp-bridge module Chaoyi Chen
2026-10-20 7:28 ` [PATCH v8 4/7] drm/bridge: aux: Add drm_aux_bridge_register_from_node() Chaoyi Chen
2026-10-20 8:35 ` sashiko-bot
2026-10-20 7:28 ` [PATCH v8 5/7] phy: rockchip: phy-rockchip-typec: Add DRM AUX bridge Chaoyi Chen
2026-10-20 8:44 ` sashiko-bot
2026-10-20 7:28 ` [PATCH v8 6/7] drm/rockchip: cdn-dp: Support handle lane info without extcon Chaoyi Chen
2026-10-20 8:52 ` sashiko-bot
2026-10-20 7:28 ` [PATCH v8 7/7] drm/rockchip: cdn-dp: Add multiple bridges to support PHY port selection Chaoyi Chen
2026-10-20 9:19 ` sashiko-bot [this message]
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-178716@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=dri-devel@lists.freedesktop.org \
--cc=kernel@airkyi.com \
--cc=linux-phy@lists.infradead.org \
--cc=neil.armstrong@linaro.org \
--cc=olteanv@gmail.com \
--cc=sashiko-reviews@lists.linux.dev \
--cc=vkoul@kernel.org \
/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