mirror of
				https://github.com/torvalds/linux.git
				synced 2025-10-31 16:48:26 +02:00 
			
		
		
		
	 19920ab98e
			
		
	
	
		19920ab98e
		
	
	
	
	
		
			
			Attempting to reload a kernel module of an HDMI driver making use of the
new CEC helpers revealed a resource deallocation issue, i.e. the entries
in /dev/cec* keep growing.
Moreover, after a couple of tries the kernel crashes and the whole
system freezes:
[   47.515950] Unable to handle kernel paging request at virtual address 0020072007200778
[...]
[   47.521707] Internal error: Oops: 0000000096000004 [#1]  SMP
[...]
[   47.537597] Call trace:
[   47.537815]  klist_next+0x20/0x1b8 (P)
[   47.538152]  device_reorder_to_tail+0x74/0x120
[   47.538548]  device_reorder_to_tail+0x6c/0x120
[   47.538944]  device_pm_move_to_tail+0x78/0xd0
[   47.539334]  deferred_probe_work_func+0x9c/0x110
[   47.539747]  process_one_work+0x328/0x638
[   47.540108]  worker_thread+0x264/0x390
[   47.540445]  kthread+0x20c/0x230
[   47.540735]  ret_from_fork+0x10/0x20
Do a proper cleanup by calling cec_unregister_adapter() instead of
cec_delete_adapter() in the managed release action handler.
Fixes: 8b1a8f8b20 ("drm/display: add CEC helpers code")
Signed-off-by: Cristian Ciocaltea <cristian.ciocaltea@collabora.com>
Reviewed-by: Dmitry Baryshkov <dmitry.baryshkov@oss.qualcomm.com>
Link: https://lore.kernel.org/r/20250703-hdmi-cec-helper-unreg-fix-v1-1-7e7b0eb578bb@collabora.com
Signed-off-by: Dmitry Baryshkov <dmitry.baryshkov@oss.qualcomm.com>
		
	
			
		
			
				
	
	
		
			193 lines
		
	
	
	
		
			5.4 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			193 lines
		
	
	
	
		
			5.4 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| // SPDX-License-Identifier: MIT
 | |
| /*
 | |
|  * Copyright (c) 2024 Linaro Ltd
 | |
|  */
 | |
| 
 | |
| #include <drm/drm_bridge.h>
 | |
| #include <drm/drm_connector.h>
 | |
| #include <drm/drm_managed.h>
 | |
| #include <drm/display/drm_hdmi_cec_helper.h>
 | |
| 
 | |
| #include <linux/export.h>
 | |
| #include <linux/mutex.h>
 | |
| 
 | |
| #include <media/cec.h>
 | |
| 
 | |
| struct drm_connector_hdmi_cec_data {
 | |
| 	struct cec_adapter *adapter;
 | |
| 	const struct drm_connector_hdmi_cec_funcs *funcs;
 | |
| };
 | |
| 
 | |
| static int drm_connector_hdmi_cec_adap_enable(struct cec_adapter *adap, bool enable)
 | |
| {
 | |
| 	struct drm_connector *connector = cec_get_drvdata(adap);
 | |
| 	struct drm_connector_hdmi_cec_data *data = connector->cec.data;
 | |
| 
 | |
| 	return data->funcs->enable(connector, enable);
 | |
| }
 | |
| 
 | |
| static int drm_connector_hdmi_cec_adap_log_addr(struct cec_adapter *adap, u8 logical_addr)
 | |
| {
 | |
| 	struct drm_connector *connector = cec_get_drvdata(adap);
 | |
| 	struct drm_connector_hdmi_cec_data *data = connector->cec.data;
 | |
| 
 | |
| 	return data->funcs->log_addr(connector, logical_addr);
 | |
| }
 | |
| 
 | |
| static int drm_connector_hdmi_cec_adap_transmit(struct cec_adapter *adap, u8 attempts,
 | |
| 						u32 signal_free_time, struct cec_msg *msg)
 | |
| {
 | |
| 	struct drm_connector *connector = cec_get_drvdata(adap);
 | |
| 	struct drm_connector_hdmi_cec_data *data = connector->cec.data;
 | |
| 
 | |
| 	return data->funcs->transmit(connector, attempts, signal_free_time, msg);
 | |
| }
 | |
| 
 | |
| static const struct cec_adap_ops drm_connector_hdmi_cec_adap_ops = {
 | |
| 	.adap_enable = drm_connector_hdmi_cec_adap_enable,
 | |
| 	.adap_log_addr = drm_connector_hdmi_cec_adap_log_addr,
 | |
| 	.adap_transmit = drm_connector_hdmi_cec_adap_transmit,
 | |
| };
 | |
| 
 | |
| static void drm_connector_hdmi_cec_adapter_phys_addr_invalidate(struct drm_connector *connector)
 | |
| {
 | |
| 	struct drm_connector_hdmi_cec_data *data = connector->cec.data;
 | |
| 
 | |
| 	cec_phys_addr_invalidate(data->adapter);
 | |
| }
 | |
| 
 | |
| static void drm_connector_hdmi_cec_adapter_phys_addr_set(struct drm_connector *connector,
 | |
| 							 u16 addr)
 | |
| {
 | |
| 	struct drm_connector_hdmi_cec_data *data = connector->cec.data;
 | |
| 
 | |
| 	cec_s_phys_addr(data->adapter, addr, false);
 | |
| }
 | |
| 
 | |
| static void drm_connector_hdmi_cec_adapter_unregister(struct drm_device *dev, void *res)
 | |
| {
 | |
| 	struct drm_connector *connector = res;
 | |
| 	struct drm_connector_hdmi_cec_data *data = connector->cec.data;
 | |
| 
 | |
| 	cec_unregister_adapter(data->adapter);
 | |
| 
 | |
| 	if (data->funcs->uninit)
 | |
| 		data->funcs->uninit(connector);
 | |
| 
 | |
| 	kfree(data);
 | |
| 	connector->cec.data = NULL;
 | |
| }
 | |
| 
 | |
| static struct drm_connector_cec_funcs drm_connector_hdmi_cec_adapter_funcs = {
 | |
| 	.phys_addr_invalidate = drm_connector_hdmi_cec_adapter_phys_addr_invalidate,
 | |
| 	.phys_addr_set = drm_connector_hdmi_cec_adapter_phys_addr_set,
 | |
| };
 | |
| 
 | |
| int drmm_connector_hdmi_cec_register(struct drm_connector *connector,
 | |
| 				     const struct drm_connector_hdmi_cec_funcs *funcs,
 | |
| 				     const char *name,
 | |
| 				     u8 available_las,
 | |
| 				     struct device *dev)
 | |
| {
 | |
| 	struct drm_connector_hdmi_cec_data *data;
 | |
| 	struct cec_connector_info conn_info;
 | |
| 	struct cec_adapter *cec_adap;
 | |
| 	int ret;
 | |
| 
 | |
| 	if (!funcs->init || !funcs->enable || !funcs->log_addr || !funcs->transmit)
 | |
| 		return -EINVAL;
 | |
| 
 | |
| 	data = kzalloc(sizeof(*data), GFP_KERNEL);
 | |
| 	if (!data)
 | |
| 		return -ENOMEM;
 | |
| 
 | |
| 	data->funcs = funcs;
 | |
| 
 | |
| 	cec_adap = cec_allocate_adapter(&drm_connector_hdmi_cec_adap_ops, connector, name,
 | |
| 					CEC_CAP_DEFAULTS | CEC_CAP_CONNECTOR_INFO,
 | |
| 					available_las ? : CEC_MAX_LOG_ADDRS);
 | |
| 	ret = PTR_ERR_OR_ZERO(cec_adap);
 | |
| 	if (ret < 0)
 | |
| 		goto err_free;
 | |
| 
 | |
| 	cec_fill_conn_info_from_drm(&conn_info, connector);
 | |
| 	cec_s_conn_info(cec_adap, &conn_info);
 | |
| 
 | |
| 	data->adapter = cec_adap;
 | |
| 
 | |
| 	mutex_lock(&connector->cec.mutex);
 | |
| 
 | |
| 	connector->cec.data = data;
 | |
| 	connector->cec.funcs = &drm_connector_hdmi_cec_adapter_funcs;
 | |
| 
 | |
| 	ret = funcs->init(connector);
 | |
| 	if (ret < 0)
 | |
| 		goto err_delete_adapter;
 | |
| 
 | |
| 	/*
 | |
| 	 * NOTE: the CEC adapter will be unregistered by drmm cleanup from
 | |
| 	 * drm_managed_release(), which is called from drm_dev_release()
 | |
| 	 * during device unbind.
 | |
| 	 *
 | |
| 	 * However, the CEC framework cleans up the CEC adapter only when the
 | |
| 	 * last user has closed its file descriptor, so we don't need to handle
 | |
| 	 * it in DRM.
 | |
| 	 *
 | |
| 	 * Before that CEC framework makes sure that even if the userspace
 | |
| 	 * still holds CEC device open, all calls will be shortcut via
 | |
| 	 * cec_is_registered(), making sure that there is no access to the
 | |
| 	 * freed memory.
 | |
| 	 */
 | |
| 	ret = cec_register_adapter(cec_adap, dev);
 | |
| 	if (ret < 0)
 | |
| 		goto err_delete_adapter;
 | |
| 
 | |
| 	mutex_unlock(&connector->cec.mutex);
 | |
| 
 | |
| 	return drmm_add_action_or_reset(connector->dev,
 | |
| 					drm_connector_hdmi_cec_adapter_unregister,
 | |
| 					connector);
 | |
| 
 | |
| err_delete_adapter:
 | |
| 	cec_delete_adapter(cec_adap);
 | |
| 
 | |
| 	connector->cec.data = NULL;
 | |
| 
 | |
| 	mutex_unlock(&connector->cec.mutex);
 | |
| 
 | |
| err_free:
 | |
| 	kfree(data);
 | |
| 
 | |
| 	return ret;
 | |
| }
 | |
| EXPORT_SYMBOL(drmm_connector_hdmi_cec_register);
 | |
| 
 | |
| void drm_connector_hdmi_cec_received_msg(struct drm_connector *connector,
 | |
| 					 struct cec_msg *msg)
 | |
| {
 | |
| 	struct drm_connector_hdmi_cec_data *data = connector->cec.data;
 | |
| 
 | |
| 	cec_received_msg(data->adapter, msg);
 | |
| }
 | |
| EXPORT_SYMBOL(drm_connector_hdmi_cec_received_msg);
 | |
| 
 | |
| void drm_connector_hdmi_cec_transmit_attempt_done(struct drm_connector *connector,
 | |
| 						  u8 status)
 | |
| {
 | |
| 	struct drm_connector_hdmi_cec_data *data = connector->cec.data;
 | |
| 
 | |
| 	cec_transmit_attempt_done(data->adapter, status);
 | |
| }
 | |
| EXPORT_SYMBOL(drm_connector_hdmi_cec_transmit_attempt_done);
 | |
| 
 | |
| void drm_connector_hdmi_cec_transmit_done(struct drm_connector *connector,
 | |
| 					  u8 status,
 | |
| 					  u8 arb_lost_cnt, u8 nack_cnt,
 | |
| 					  u8 low_drive_cnt, u8 error_cnt)
 | |
| {
 | |
| 	struct drm_connector_hdmi_cec_data *data = connector->cec.data;
 | |
| 
 | |
| 	cec_transmit_done(data->adapter, status,
 | |
| 			  arb_lost_cnt, nack_cnt, low_drive_cnt, error_cnt);
 | |
| }
 | |
| EXPORT_SYMBOL(drm_connector_hdmi_cec_transmit_done);
 |