This series is an attempt to fix the unbind/bind crash (due to an use-after-free bug) found on rockchip-drm driver.
The problem lies in the way the driver uses the component API. Currently, rockchip_drm_unbind calls component_unbind_all before drm_mode_config_cleanup, the former releasing the memory where the DRM objects are embedded.
The series goal is basically to fix all the components, making proper use of the respective .destroy hooks, making sure there are no use-after-free or double-free issues.
The first patch is likely the most controversial, which is required because component_bind_all will call component_unbind without calling drm_mode_config_cleanup, if any component fails to bind. As mentioned above, this is problematic in the DRM framework.
Thanks! Ezequiel
Ezequiel Garcia (5): component: Add an API to cleanup before unbind drm/rockchip: Fix the device unbind order drm/rockchip: vop: Fix CRTC unbind drm/rockchip: lvds: Fix component unbind drm/rockchip: rk3066_hdmi: Cleanup component unbind
drivers/base/component.c | 9 +++- drivers/gpu/drm/rockchip/rk3066_hdmi.c | 8 +-- drivers/gpu/drm/rockchip/rockchip_drm_drv.c | 20 +++++--- drivers/gpu/drm/rockchip/rockchip_drm_vop.c | 56 ++++++++------------- drivers/gpu/drm/rockchip/rockchip_lvds.c | 20 ++++---- include/linux/component.h | 10 +++- 6 files changed, 60 insertions(+), 63 deletions(-)
Some users of the component API have a special model for the allocation and release of its resources: resources are allocated by an API but then released by other means.
This contrasts with the current component API assumption: .unbind must undo everything that .bind did.
An example of this is the DRM framework, which expects registered objects (connectors, planes, CRTCs, etc) to be released by respective drm_xxx_funcs.destroy hooks.
The drm_xxx_funcs.destroy call is done either directly by drm_mode_config_cleanup() or in a refcounted fashion, depending on the type of object.
For example, a DRM CRTC object is registered by drm_crtc_init_with_planes(), and then released by drm_crtc_cleanup(), which is normally called from the drm_crtc_funcs.destroy hook.
Now, in this model, drm_mode_config_cleanup() should always be called before component_unbind() to avoid use-after-free situations (because each component has a devres group).
However, component_bind_all() calls component_unbind on binded components, if any component in the chain fails to bind.
In order to allow this special case, and following Alan Kay:
"simple things should be simple, complex things should be possible"
introduce an extension to component_bind_all, which takes an extra cleanup callback, to be called when binding fails to perform extra cleanup steps.
This new API allows the following simple pattern:
void unbind_cleanup(...) { drm_mode_config_cleanup(drm_dev); }
int foo_bind() { component_bind_all_or_cleanup(dev, drm_dev, unbind_cleanup); }
void foo_unbind() { drm_mode_config_cleanup(drm_dev); component_unbind_all(dev, drm_dev); }
Each DRM component then uses the respective .destroy hooks to destroy DRM resources, and the .unbind hooks to release non-DRM resources.
Arguably, this could be viewed as Very Ugly. However, it handles this complex case, making it possible to fix the current unbind crashes that some DRM drivers suffer from, in a non-invasive way, keeping the DRM resource handling model.
Signed-off-by: Ezequiel Garcia ezequiel@collabora.com --- drivers/base/component.c | 9 +++++++-- include/linux/component.h | 10 +++++++++- 2 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/drivers/base/component.c b/drivers/base/component.c index 532a3a5d8f63..371cff9208cf 100644 --- a/drivers/base/component.c +++ b/drivers/base/component.c @@ -622,12 +622,14 @@ static int component_bind(struct component *component, struct master *master, * component_bind_all - bind all components of an aggregate driver * @master_dev: device with the aggregate driver * @data: opaque pointer, passed to all components + * @cleanup: optional cleanup callback. * * Binds all components of the aggregate @dev by passing @data to their * &component_ops.bind functions. Should be called from * &component_master_ops.bind. */ -int component_bind_all(struct device *master_dev, void *data) +int component_bind_all_or_cleanup(struct device *master_dev, + void *data, void (*cleanup)(void *data)) { struct master *master; struct component *c; @@ -650,6 +652,9 @@ int component_bind_all(struct device *master_dev, void *data) }
if (ret != 0) { + if (cleanup) + cleanup(data); + for (; i > 0; i--) if (!master->match->compare[i - 1].duplicate) { c = master->match->compare[i - 1].component; @@ -659,7 +664,7 @@ int component_bind_all(struct device *master_dev, void *data)
return ret; } -EXPORT_SYMBOL_GPL(component_bind_all); +EXPORT_SYMBOL_GPL(component_bind_all_or_cleanup);
static int __component_add(struct device *dev, const struct component_ops *ops, int subcomponent) diff --git a/include/linux/component.h b/include/linux/component.h index 16de18f473d7..1a5c7b772de3 100644 --- a/include/linux/component.h +++ b/include/linux/component.h @@ -38,7 +38,15 @@ int component_add_typed(struct device *dev, const struct component_ops *ops, int subcomponent); void component_del(struct device *, const struct component_ops *);
-int component_bind_all(struct device *master, void *master_data); +int component_bind_all_or_cleanup(struct device *master, + void *master_data, + void (*cleanup)(void *data)); + +static inline int component_bind_all(struct device *master, void *master_data) +{ + return component_bind_all_or_cleanup(master, master_data, NULL); +} + void component_unbind_all(struct device *master, void *master_data);
struct master;
In order to cleanup the configuration, destroying the components in the pipeline, the components must be present.
Therefore, cleanup the config first, and unbind the components later.
Signed-off-by: Ezequiel Garcia ezequiel@collabora.com --- drivers/gpu/drm/rockchip/rockchip_drm_drv.c | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-)
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_drv.c b/drivers/gpu/drm/rockchip/rockchip_drm_drv.c index 20ecb1508a22..ca12a35483f9 100644 --- a/drivers/gpu/drm/rockchip/rockchip_drm_drv.c +++ b/drivers/gpu/drm/rockchip/rockchip_drm_drv.c @@ -108,6 +108,11 @@ static void rockchip_iommu_cleanup(struct drm_device *drm_dev) iommu_domain_free(private->domain); }
+static void unbind_cleanup(void *data) +{ + drm_mode_config_cleanup((struct drm_device *)data); +} + static int rockchip_drm_bind(struct device *dev) { struct drm_device *drm_dev; @@ -140,13 +145,13 @@ static int rockchip_drm_bind(struct device *dev) rockchip_drm_mode_config_init(drm_dev);
/* Try to bind all sub drivers. */ - ret = component_bind_all(dev, drm_dev); + ret = component_bind_all_or_cleanup(dev, drm_dev, unbind_cleanup); if (ret) - goto err_mode_config_cleanup; + goto err_free;
ret = drm_vblank_init(drm_dev, drm_dev->mode_config.num_crtc); if (ret) - goto err_unbind_all; + goto err_drm_cleanup;
drm_mode_config_reset(drm_dev);
@@ -158,7 +163,7 @@ static int rockchip_drm_bind(struct device *dev)
ret = rockchip_drm_fbdev_init(drm_dev); if (ret) - goto err_unbind_all; + goto err_drm_cleanup;
/* init kms poll for handling hpd */ drm_kms_helper_poll_init(drm_dev); @@ -171,10 +176,9 @@ static int rockchip_drm_bind(struct device *dev) err_kms_helper_poll_fini: drm_kms_helper_poll_fini(drm_dev); rockchip_drm_fbdev_fini(drm_dev); -err_unbind_all: - component_unbind_all(dev, drm_dev); -err_mode_config_cleanup: +err_drm_cleanup: drm_mode_config_cleanup(drm_dev); + component_unbind_all(dev, drm_dev); rockchip_iommu_cleanup(drm_dev); err_free: drm_dev->dev_private = NULL; @@ -193,8 +197,8 @@ static void rockchip_drm_unbind(struct device *dev) drm_kms_helper_poll_fini(drm_dev);
drm_atomic_helper_shutdown(drm_dev); - component_unbind_all(dev, drm_dev); drm_mode_config_cleanup(drm_dev); + component_unbind_all(dev, drm_dev); rockchip_iommu_cleanup(drm_dev);
drm_dev->dev_private = NULL;
In order to fix device unbinding, the CRTC release path needs to be fixed. Get rid of the use-after-free issue that arise for calling drm_crtc_cleanup() prematurely, by moving all the CRTC resource release to the crtc.destroy() hook.
The vop_unbind() function is only responsible for the release of driver-specific (i.e. vop-specific) resources.
Signed-off-by: Ezequiel Garcia ezequiel@collabora.com --- drivers/gpu/drm/rockchip/rockchip_drm_vop.c | 56 ++++++++------------- 1 file changed, 20 insertions(+), 36 deletions(-)
diff --git a/drivers/gpu/drm/rockchip/rockchip_drm_vop.c b/drivers/gpu/drm/rockchip/rockchip_drm_vop.c index d04b3492bdac..87c43097da7e 100644 --- a/drivers/gpu/drm/rockchip/rockchip_drm_vop.c +++ b/drivers/gpu/drm/rockchip/rockchip_drm_vop.c @@ -1387,6 +1387,11 @@ static const struct drm_crtc_helper_funcs vop_crtc_helper_funcs = {
static void vop_crtc_destroy(struct drm_crtc *crtc) { + struct vop *vop = to_vop(crtc); + + drm_flip_work_cleanup(&vop->fb_unref_work); + drm_self_refresh_helper_cleanup(crtc); + of_node_put(crtc->port); drm_crtc_cleanup(crtc); }
@@ -1606,12 +1611,22 @@ static void vop_plane_add_properties(struct drm_plane *plane, DRM_MODE_ROTATE_0 | flags); }
+static void vop_plane_cleanup(struct vop *vop) +{ + struct drm_device *drm_dev = vop->drm_dev; + struct drm_plane *plane, *tmp; + + list_for_each_entry_safe(plane, tmp, &drm_dev->mode_config.plane_list, + head) + drm_plane_cleanup(plane); +} + static int vop_create_crtc(struct vop *vop) { const struct vop_data *vop_data = vop->data; struct device *dev = vop->dev; struct drm_device *drm_dev = vop->drm_dev; - struct drm_plane *primary = NULL, *cursor = NULL, *plane, *tmp; + struct drm_plane *primary = NULL, *cursor = NULL; struct drm_crtc *crtc = &vop->crtc; struct device_node *port; int ret; @@ -1625,6 +1640,7 @@ static int vop_create_crtc(struct vop *vop) for (i = 0; i < vop_data->win_size; i++) { struct vop_win *vop_win = &vop->win[i]; const struct vop_win_data *win_data = vop_win->data; + struct drm_plane *plane;
if (win_data->type != DRM_PLANE_TYPE_PRIMARY && win_data->type != DRM_PLANE_TYPE_CURSOR) @@ -1714,42 +1730,10 @@ static int vop_create_crtc(struct vop *vop) err_cleanup_crtc: drm_crtc_cleanup(crtc); err_cleanup_planes: - list_for_each_entry_safe(plane, tmp, &drm_dev->mode_config.plane_list, - head) - drm_plane_cleanup(plane); + vop_plane_cleanup(vop); return ret; }
-static void vop_destroy_crtc(struct vop *vop) -{ - struct drm_crtc *crtc = &vop->crtc; - struct drm_device *drm_dev = vop->drm_dev; - struct drm_plane *plane, *tmp; - - drm_self_refresh_helper_cleanup(crtc); - - of_node_put(crtc->port); - - /* - * We need to cleanup the planes now. Why? - * - * The planes are "&vop->win[i].base". That means the memory is - * all part of the big "struct vop" chunk of memory. That memory - * was devm allocated and associated with this component. We need to - * free it ourselves before vop_unbind() finishes. - */ - list_for_each_entry_safe(plane, tmp, &drm_dev->mode_config.plane_list, - head) - vop_plane_destroy(plane); - - /* - * Destroy CRTC after vop_plane_destroy() since vop_disable_plane() - * references the CRTC. - */ - drm_crtc_cleanup(crtc); - drm_flip_work_cleanup(&vop->fb_unref_work); -} - static int vop_initial(struct vop *vop) { struct reset_control *ahb_rst; @@ -2020,7 +2004,8 @@ static int vop_bind(struct device *dev, struct device *master, void *data)
err_disable_pm_runtime: pm_runtime_disable(&pdev->dev); - vop_destroy_crtc(vop); + vop_plane_cleanup(vop); + vop_crtc_destroy(&vop->crtc); return ret; }
@@ -2032,7 +2017,6 @@ static void vop_unbind(struct device *dev, struct device *master, void *data) rockchip_rgb_fini(vop->rgb);
pm_runtime_disable(dev); - vop_destroy_crtc(vop);
clk_unprepare(vop->aclk); clk_unprepare(vop->hclk);
Remove the explicit encoder disable: it's already part of the CRTC shutdown, in both atomic and legacy API cases.
Also, encoder and connector cleanup is performed by the DRM .destroy hooks, for encoder and connector respectively. It can be removed as well.
Finally, move the panel detach call to the connector .destroy hook, where it belongs.
Signed-off-by: Ezequiel Garcia ezequiel@collabora.com --- drivers/gpu/drm/rockchip/rockchip_lvds.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/drivers/gpu/drm/rockchip/rockchip_lvds.c b/drivers/gpu/drm/rockchip/rockchip_lvds.c index f25a36743cbd..154f14a317d7 100644 --- a/drivers/gpu/drm/rockchip/rockchip_lvds.c +++ b/drivers/gpu/drm/rockchip/rockchip_lvds.c @@ -100,9 +100,18 @@ static inline int rockchip_lvds_name_to_output(const char *s) return -EINVAL; }
+static void +rockchip_lvds_connector_destroy(struct drm_connector *connector) +{ + struct rockchip_lvds *lvds = connector_to_lvds(connector); + + drm_panel_detach(lvds->panel); + drm_connector_cleanup(connector); +} + static const struct drm_connector_funcs rockchip_lvds_connector_funcs = { .fill_modes = drm_helper_probe_single_connector_modes, - .destroy = drm_connector_cleanup, + .destroy = rockchip_lvds_connector_destroy, .reset = drm_atomic_helper_connector_reset, .atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state, .atomic_destroy_state = drm_atomic_helper_connector_destroy_state, @@ -675,16 +684,7 @@ static int rockchip_lvds_bind(struct device *dev, struct device *master, static void rockchip_lvds_unbind(struct device *dev, struct device *master, void *data) { - struct rockchip_lvds *lvds = dev_get_drvdata(dev); - const struct drm_encoder_helper_funcs *encoder_funcs; - - encoder_funcs = lvds->soc_data->helper_funcs; - encoder_funcs->disable(&lvds->encoder); - if (lvds->panel) - drm_panel_detach(lvds->panel); pm_runtime_disable(dev); - drm_connector_cleanup(&lvds->connector); - drm_encoder_cleanup(&lvds->encoder); }
static const struct component_ops rockchip_lvds_component_ops = {
Remove drm_connector_unregister() since it should be used only by drivers that call drm_dev_register explicitly.
Also, call the DRM cleanups directly, instead of (ab)using the destroy hooks, for readability reasons.
Signed-off-by: Ezequiel Garcia ezequiel@collabora.com --- drivers/gpu/drm/rockchip/rk3066_hdmi.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/drivers/gpu/drm/rockchip/rk3066_hdmi.c b/drivers/gpu/drm/rockchip/rk3066_hdmi.c index fe203d38664e..5a2d62a2cf50 100644 --- a/drivers/gpu/drm/rockchip/rk3066_hdmi.c +++ b/drivers/gpu/drm/rockchip/rk3066_hdmi.c @@ -518,7 +518,6 @@ rk3066_hdmi_probe_single_connector_modes(struct drm_connector *connector,
static void rk3066_hdmi_connector_destroy(struct drm_connector *connector) { - drm_connector_unregister(connector); drm_connector_cleanup(connector); }
@@ -819,8 +818,8 @@ static int rk3066_hdmi_bind(struct device *dev, struct device *master, return 0;
err_cleanup_hdmi: - hdmi->connector.funcs->destroy(&hdmi->connector); - hdmi->encoder.funcs->destroy(&hdmi->encoder); + drm_connector_cleanup(&hdmi->connector); + drm_encoder_cleanup(&hdmi->encoder); err_disable_i2c: i2c_put_adapter(hdmi->ddc); err_disable_hclk: @@ -834,9 +833,6 @@ static void rk3066_hdmi_unbind(struct device *dev, struct device *master, { struct rk3066_hdmi *hdmi = dev_get_drvdata(dev);
- hdmi->connector.funcs->destroy(&hdmi->connector); - hdmi->encoder.funcs->destroy(&hdmi->encoder); - i2c_put_adapter(hdmi->ddc); clk_disable_unprepare(hdmi->hclk); }
dri-devel@lists.freedesktop.org