[PATCH 07/10] drm: Revamp connector_list protection
Daniel Vetter
daniel.vetter at ffwll.ch
Tue Jun 21 09:10:32 UTC 2016
This is a pretty good horror show, but I think it's the best tradeoff:
- Thanks to srcu and delayed freeing the locking doesn't leak out to
callers, hence no added headaches with locking inversions.
- For core and drivers which hot-remove connectors all the connector
list walking details are hidden in a macro.
- Other drivers which don't care about hot-removing can simply keep on
using the normal list walking macros.
The new design is:
- New dev->mode_config.connector_list_lock to protect the
connector_list, and the connector_ida (since that's also
unprotected), plus num_connectors. This is a pure leaf lock, nothing
is allowed to nest within, nothing outside of connector init/cleanup
will ever need it.
- Protect connector_list itself with srcu. This allows sleeping and
anything else. The only thing which is not allowed is calling
synchronize_srcu, or grabbing any locks or waiting on
waitqueues/completions/whatever which might call that. To make this
100% safe we opt to not have any calls to synchronize_srcu.
- Protect against use-after-free of connectors using call_srcu, to
delay the kfree enough.
- To protect against zombie connectors which are in progress of final
destruction use kref_get_unless_zero. This is safe since srcu
prevents against use-after-free, and that's the only guarantee we
need.
For this demo I only bothered converting i915, and there also not
everything - I left the connector loops in the modeset code unchanged
since those will all be converted over to
drm_for_each_connector_in_state to make them work correctly for
nonblocking atomic commits. Only loops outside of atomic commits
should be converted to drm_for_each_connector.
Note that the i915 DP MST code still uses drm_modeset_lock_all(). But
that's not because of drm core connector lifetime issues, but because
the fbdev helper reuses core locks to mange its own lists and data
structures. Thierry Reding has a patch series to gift fbdev its own
lock, which will remedy this.
v2: Totally rewrite everything to pack it up in a neat
iterator macro, with init/check/next extracted into helpers.
v3: Tiny rebase conflict.
Signed-off-by: Daniel Vetter <daniel.vetter at intel.com>
---
drivers/gpu/drm/drm_crtc.c | 57 +++++++++++++++++++++-----
drivers/gpu/drm/i915/intel_dp_mst.c | 2 +-
include/drm/drm_crtc.h | 82 +++++++++++++++++++++++++++----------
3 files changed, 109 insertions(+), 32 deletions(-)
diff --git a/drivers/gpu/drm/drm_crtc.c b/drivers/gpu/drm/drm_crtc.c
index 6a8f91e8821b..dc22c0bfbe99 100644
--- a/drivers/gpu/drm/drm_crtc.c
+++ b/drivers/gpu/drm/drm_crtc.c
@@ -44,6 +44,9 @@
#include "drm_crtc_internal.h"
#include "drm_internal.h"
+DEFINE_SRCU(drm_connector_list_srcu);
+EXPORT_SYMBOL(drm_connector_list_srcu);
+
static struct drm_framebuffer *
internal_framebuffer_create(struct drm_device *dev,
const struct drm_mode_fb_cmd2 *r,
@@ -825,7 +828,7 @@ static void drm_connector_get_cmdline_mode(struct drm_connector *connector)
mode->interlace ? " interlaced" : "");
}
-static void drm_connector_free(struct kref *kref)
+static void drm_connector_kref_release(struct kref *kref)
{
struct drm_connector *connector =
container_of(kref, struct drm_connector, base.refcount);
@@ -858,11 +861,10 @@ int drm_connector_init(struct drm_device *dev,
struct ida *connector_ida =
&drm_connector_enum_list[connector_type].ida;
- drm_modeset_lock_all(dev);
-
+ mutex_lock(&dev->mode_config.connector_list_lock);
ret = drm_mode_object_get_reg(dev, &connector->base,
DRM_MODE_OBJECT_CONNECTOR,
- false, drm_connector_free);
+ false, drm_connector_kref_release);
if (ret)
goto out_unlock;
@@ -899,11 +901,6 @@ int drm_connector_init(struct drm_device *dev,
drm_connector_get_cmdline_mode(connector);
- /* We should add connectors at the end to avoid upsetting the connector
- * index too much. */
- list_add_tail(&connector->head, &config->connector_list);
- config->num_connector++;
-
if (connector_type != DRM_MODE_CONNECTOR_VIRTUAL)
drm_object_attach_property(&connector->base,
config->edid_property,
@@ -917,6 +914,11 @@ int drm_connector_init(struct drm_device *dev,
}
connector->debugfs_entry = NULL;
+
+ /* We must add the connector to the list last. */
+ list_add_tail_rcu(&connector->head, &config->connector_list);
+ config->num_connector++;
+
out_put_type_id:
if (ret)
ida_remove(connector_ida, connector->connector_type_id);
@@ -928,7 +930,7 @@ out_put:
drm_mode_object_unregister(dev, &connector->base);
out_unlock:
- drm_modeset_unlock_all(dev);
+ mutex_unlock(&dev->mode_config.connector_list_lock);
return ret;
}
@@ -951,6 +953,7 @@ void drm_connector_cleanup(struct drm_connector *connector)
if (WARN_ON(connector->registered))
drm_connector_unregister(connector);
+ mutex_lock(&dev->mode_config.connector_list_lock);
if (connector->tile_group) {
drm_mode_put_tile_group(dev, connector->tile_group);
connector->tile_group = NULL;
@@ -981,9 +984,42 @@ void drm_connector_cleanup(struct drm_connector *connector)
connector->state);
memset(connector, 0, sizeof(*connector));
+ mutex_unlock(&dev->mode_config.connector_list_lock);
}
EXPORT_SYMBOL(drm_connector_cleanup);
+static void drm_connector_free_cb(struct rcu_head *head)
+{
+ struct drm_connector *connector = container_of(head,
+ struct drm_connector,
+ free_head);
+
+ kfree(connector);
+}
+
+/**
+ * drm_connector_free - frees a connector structure
+ * @connector: connector to free
+ *
+ * This frees @connector using call_srcu() and kfree(). If the driver subclasses
+ * the connector, then the embedded struct &drm_connector must be the first
+ * element, and @connector must have been allocated using kmalloc() or one of
+ * the functions wrapping that.
+ *
+ * Delayed freeing is required to avoid use-after-free when walking the
+ * connector list, which is srcu protected. Hence drivers must use this function
+ * for connectors which can be removed while the driver is loaded. Currently
+ * that's only true for DP MST connectors. For any other connectors it is valid
+ * to call kfree directly.
+ */
+void drm_connector_free(struct drm_connector *connector)
+{
+ call_srcu(&drm_connector_list_srcu,
+ &connector->free_head,
+ drm_connector_free_cb);
+}
+EXPORT_SYMBOL(drm_connector_free);
+
/**
* drm_connector_register - register a connector
* @connector: the connector to register
@@ -5554,6 +5590,7 @@ void drm_mode_config_init(struct drm_device *dev)
mutex_init(&dev->mode_config.idr_mutex);
mutex_init(&dev->mode_config.fb_lock);
mutex_init(&dev->mode_config.blob_lock);
+ mutex_init(&dev->mode_config.connector_list_lock);
INIT_LIST_HEAD(&dev->mode_config.fb_list);
INIT_LIST_HEAD(&dev->mode_config.crtc_list);
INIT_LIST_HEAD(&dev->mode_config.connector_list);
diff --git a/drivers/gpu/drm/i915/intel_dp_mst.c b/drivers/gpu/drm/i915/intel_dp_mst.c
index 9646816604be..11bbbb194f2d 100644
--- a/drivers/gpu/drm/i915/intel_dp_mst.c
+++ b/drivers/gpu/drm/i915/intel_dp_mst.c
@@ -327,7 +327,7 @@ intel_dp_mst_connector_destroy(struct drm_connector *connector)
kfree(intel_connector->edid);
drm_connector_cleanup(connector);
- kfree(connector);
+ drm_connector_free(connector);
}
static const struct drm_connector_funcs intel_dp_mst_connector_funcs = {
diff --git a/include/drm/drm_crtc.h b/include/drm/drm_crtc.h
index ba574b9c1ec4..8f4380ac113e 100644
--- a/include/drm/drm_crtc.h
+++ b/include/drm/drm_crtc.h
@@ -32,6 +32,7 @@
#include <linux/fb.h>
#include <linux/hdmi.h>
#include <linux/media-bus-format.h>
+#include <linux/srcu.h>
#include <uapi/drm/drm_mode.h>
#include <uapi/drm/drm_fourcc.h>
#include <drm/drm_modeset_lock.h>
@@ -1186,6 +1187,7 @@ struct drm_encoder {
* @kdev: kernel device for sysfs attributes
* @attr: sysfs attributes
* @head: list management
+ * @free_head: rcu delayed freeing management
* @base: base KMS object
* @name: human readable name, can be overwritten by the driver
* @connector_id: compacted connector id useful indexing arrays
@@ -1241,6 +1243,7 @@ struct drm_connector {
struct device *kdev;
struct device_attribute *attr;
struct list_head head;
+ struct rcu_head free_head;
struct drm_mode_object base;
@@ -2309,7 +2312,7 @@ struct drm_mode_config {
* @tile_idr:
*
* Use this idr for allocating new IDs for tiled sinks like use in some
- * high-res DP MST screens.
+ * high-res DP MST screens. Protected by @idr_mutex.
*/
struct idr tile_idr;
@@ -2320,6 +2323,14 @@ struct drm_mode_config {
int num_connector;
struct ida connector_ida;
struct list_head connector_list;
+
+ /**
+ * @connector_list_lock:
+ *
+ * Protects @connector_list, @connector_ida and * @num_conncetors.
+ */
+ struct mutex connector_list_lock;
+
int num_encoder;
struct list_head encoder_list;
@@ -2426,6 +2437,8 @@ struct drm_mode_config {
struct drm_mode_config_helper_funcs *helper_private;
};
+extern struct srcu_struct drm_connector_list_srcu;
+
/**
* drm_for_each_plane_mask - iterate over planes specified by bitmask
* @plane: the loop cursor
@@ -2505,6 +2518,7 @@ int drm_connector_register(struct drm_connector *connector);
void drm_connector_unregister(struct drm_connector *connector);
extern void drm_connector_cleanup(struct drm_connector *connector);
+extern void drm_connector_free(struct drm_connector *connector);
static inline unsigned drm_connector_index(struct drm_connector *connector)
{
return connector->connector_id;
@@ -2835,31 +2849,57 @@ static inline void drm_connector_unreference(struct drm_connector *connector)
#define drm_for_each_crtc(crtc, dev) \
list_for_each_entry(crtc, &(dev)->mode_config.crtc_list, head)
+struct drm_connector_iter {
+ struct drm_mode_config *mode_config;
+ struct drm_connector *connector;
+ int srcu_ret;
+};
+
static inline void
-assert_drm_connector_list_read_locked(struct drm_mode_config *mode_config)
+__drm_connector_iter_init(struct drm_mode_config *mode_config,
+ struct drm_connector_iter *iter)
{
- /*
- * The connector hotadd/remove code currently grabs both locks when
- * updating lists. Hence readers need only hold either of them to be
- * safe and the check amounts to
- *
- * WARN_ON(not_holding(A) && not_holding(B)).
- */
- WARN_ON(!mutex_is_locked(&mode_config->mutex) &&
- !drm_modeset_is_locked(&mode_config->connection_mutex));
+ iter->mode_config = mode_config;
+ iter->srcu_ret = srcu_read_lock(&drm_connector_list_srcu);
+ iter->connector = list_entry_rcu(mode_config->connector_list.next,
+ struct drm_connector, head);
}
-struct drm_connector_iter {
- struct drm_mode_config *mode_config;
-};
+static inline struct drm_connector *
+__drm_connector_iter_check(struct drm_connector_iter *iter)
+{
+retry:
+ if (&iter->connector->head == &iter->mode_config->connector_list) {
+ /* last iteration, clean up */
+ srcu_read_unlock(&drm_connector_list_srcu, iter->srcu_ret);
+ return NULL;
+ }
+
+ /* srcu protects against iter->connector disappearing */
+ if (!kref_get_unless_zero(&iter->connector->base.refcount)) {
+ iter->connector = list_entry_rcu(iter->connector->head.next,
+ struct drm_connector, head);
+ goto retry;
+ }
+
+ return iter->connector;
+}
+
+static inline void
+__drm_connector_iter_next(struct drm_connector_iter *iter)
+{
+ /* _next() is only called when _check() succeeded on iter->connector,
+ * and _check() only succeeds if kref_get_unless_zero() succeeded.
+ * Therefore dropping the reference here is the correct place. */
+ drm_connector_unreference(iter->connector);
+ iter->connector = list_entry_rcu(iter->connector->head.next,
+ struct drm_connector, head);
+}
-#define drm_for_each_connector(connector, dev, iter) \
- for ((iter).mode_config = NULL, \
- assert_drm_connector_list_read_locked(&(dev)->mode_config), \
- connector = list_first_entry(&(dev)->mode_config.connector_list, \
- struct drm_connector, head); \
- &connector->head != (&(dev)->mode_config.connector_list); \
- connector = list_next_entry(connector, head))
+#define drm_for_each_connector(connector, dev, iter) \
+ for (__drm_connector_iter_init(&(dev)->mode_config, &(iter)); \
+ (connector = __drm_connector_iter_check(&(iter))); \
+ __drm_connector_iter_next(&(iter)))
#define drm_for_each_encoder(encoder, dev) \
list_for_each_entry(encoder, &(dev)->mode_config.encoder_list, head)
--
2.8.1
More information about the dri-devel
mailing list