* [PATCH v2 1/1] crypto: atmel-ecc - fix multi-device use-after-free and registration races
@ 2026-06-06 20:11 Lothar Rubusch
0 siblings, 0 replies; only message in thread
From: Lothar Rubusch @ 2026-06-06 20:11 UTC (permalink / raw)
To: thorsten.blum, herbert, davem, nicolas.ferre, alexandre.belloni,
claudiu.beznea, tudor.ambarus, krzk+dt
Cc: linux-crypto, linux-arm-kernel, linux-kernel, l.rubusch
During parallel driver initialization or driver teardown sequences
in setups with multiple atmel-ecc instances, a race condition exists
between atmel_ecc_i2c_client_alloc() and the probe/remove paths.
A concurrent transformation request can fetch an i2c_client instance
from the global i2c_client_list before the kpp is fully registered, or
while it is actively being unbound, resulting in a use-after-free (UAF)
risk.
1. The initialization problem in probe(): Adding first an i2c client to the
i2c_client_list, and then registering the kpp algorim may result in a race,
when this happens for a second (or further) probed device. In this case the
algorithm is already registered, so a TFM may arrive, while the latest
probing device is added to the list, but not kpp registered. In case this
fails and this last device is going to be removed again from the list, this
leaves a window where the TFM might obtain a pointer to the - now deleted -
i2c client, which opens a UAF risk. Furthermore, there will happen atempts
to multiple registering the same driver to the same type of algorithm.
Note, a simple reverting of the order: first register kpp, second add the
i2c client to the i2c_client_list - is not possible here, since the kpp
registration immediately triggers the self tests, which then will allocate
and require an i2c client.
2. The removal problem, also related to the re-initialization in particular
scenarios where this might happen to quick, might result in overwriting not
fully freed memory resources. The remove might still take time, a
re-probing then overwrite the (still existing) static resource without
having it before cleaned up before. Here were additional issues with the
order of when to remove the i2c client from the i2c_client_list but already
having removed the resources.
Address this by implementing an independent subsystem reference counter
kpp refcnt protected by a dedicated mutex to ensure the static global kpp
algorithm structure is registered exactly once by the first probing device
instance. In multi-device scenarios, or when extending the resource
management support of the i2c_client_list to all atmel-i2c based device
drivers, such scenarios can become realistic. The particular algorithm is
registered only once. Each i2c client (i.e. each probing device driver) is
added as client to the i2c_client_list. This guarantee that only the first
probe will register the algorithm. The list is populated for further calls
to probe, and subsequent calls to the client alloc function.
Concurrently, decouple list mutations from registration by moving the
global list eviction to the absolute top of the remove lifecycle. This
keeps the quick execution of the list allocation loop intact, ensures that
unbinding hardware is instantly blind to the rest of the system, and
completely bypasses the recursive deadlock condition previously triggered
by synchronous crypto API self-tests.
Fixes: 11105693fa05 ("crypto: atmel-ecc - introduce Microchip / Atmel ECC driver")
Signed-off-by: Lothar Rubusch <l.rubusch@gmail.com>
---
v1 -> v2:
- remove the initial approach with "ready" state bool, replace it by
this be a more comprehensive approach
drivers/crypto/atmel-ecc.c | 96 +++++++++++++++++++++++++++-----------
drivers/crypto/atmel-i2c.h | 1 +
2 files changed, 70 insertions(+), 27 deletions(-)
diff --git a/drivers/crypto/atmel-ecc.c b/drivers/crypto/atmel-ecc.c
index 0ca02995a1de..0f1567bf23b7 100644
--- a/drivers/crypto/atmel-ecc.c
+++ b/drivers/crypto/atmel-ecc.c
@@ -23,6 +23,11 @@
#include <crypto/kpp.h>
#include "atmel-i2c.h"
+static DEFINE_MUTEX(atmel_ecc_kpp_lock);
+static int atmel_ecc_kpp_refcnt;
+DECLARE_COMPLETION(atmel_ecc_unreg_done);
+static bool atmel_ecc_unreg_active;
+
static struct atmel_ecc_driver_data driver_data;
/**
@@ -241,7 +246,8 @@ static void atmel_ecc_i2c_client_free(struct i2c_client *client)
{
struct atmel_i2c_client_priv *i2c_priv = i2c_get_clientdata(client);
- atomic_dec(&i2c_priv->tfm_count);
+ if (atomic_dec_and_test(&i2c_priv->tfm_count))
+ complete(&i2c_priv->remove_done);
}
static int atmel_ecdh_init_tfm(struct crypto_kpp *tfm)
@@ -276,7 +282,8 @@ static void atmel_ecdh_exit_tfm(struct crypto_kpp *tfm)
struct atmel_ecdh_ctx *ctx = kpp_tfm_ctx(tfm);
kfree(ctx->public_key);
- crypto_free_kpp(ctx->fallback);
+ if (ctx->fallback)
+ crypto_free_kpp(ctx->fallback);
atmel_ecc_i2c_client_free(ctx->client);
}
@@ -315,6 +322,7 @@ static struct kpp_alg atmel_ecdh_nist_p256 = {
static int atmel_ecc_probe(struct i2c_client *client)
{
struct atmel_i2c_client_priv *i2c_priv;
+ unsigned long timeout;
int ret;
ret = atmel_i2c_probe(client);
@@ -328,44 +336,78 @@ static int atmel_ecc_probe(struct i2c_client *client)
&driver_data.i2c_client_list);
spin_unlock(&driver_data.i2c_list_lock);
- ret = crypto_register_kpp(&atmel_ecdh_nist_p256);
- if (ret) {
- spin_lock(&driver_data.i2c_list_lock);
- list_del(&i2c_priv->i2c_client_list_node);
- spin_unlock(&driver_data.i2c_list_lock);
+ mutex_lock(&atmel_ecc_kpp_lock);
+ /*
+ * For cases where the same/last such device is still in unregistering,
+ * and now re-registering (refcnt is 0, but completion still exists).
+ * Safely capture the pointer, drop the lock and sleep until it
+ * terminates upon completion or retry limit reached.
+ */
+ while (atmel_ecc_unreg_active) {
+ mutex_unlock(&atmel_ecc_kpp_lock);
+ timeout = wait_for_completion_timeout(&atmel_ecc_unreg_done,
+ msecs_to_jiffies(2000));
+ mutex_lock(&atmel_ecc_kpp_lock);
+
+ if (timeout == 0) {
+ spin_lock(&driver_data.i2c_list_lock);
+ list_del(&i2c_priv->i2c_client_list_node);
+ spin_unlock(&driver_data.i2c_list_lock);
+ mutex_unlock(&atmel_ecc_kpp_lock);
+
+ dev_err(&client->dev, "probe timed out, former driver instance not fully deregistered\n");
+ return -ETIMEDOUT;
+ }
+ }
- dev_err(&client->dev, "%s alg registration failed\n",
- atmel_ecdh_nist_p256.base.cra_driver_name);
- } else {
- dev_info(&client->dev, "atmel ecc algorithms registered in /proc/crypto\n");
+ if (atmel_ecc_kpp_refcnt == 0) {
+ ret = crypto_register_kpp(&atmel_ecdh_nist_p256);
+ if (ret) {
+ spin_lock(&driver_data.i2c_list_lock);
+ list_del(&i2c_priv->i2c_client_list_node);
+ spin_unlock(&driver_data.i2c_list_lock);
+ mutex_unlock(&atmel_ecc_kpp_lock);
+
+ dev_err(&client->dev, "%s alg registration failed\n",
+ atmel_ecdh_nist_p256.base.cra_driver_name);
+ return ret;
+ }
}
+ atmel_ecc_kpp_refcnt++;
+ mutex_unlock(&atmel_ecc_kpp_lock);
+ dev_info(&client->dev, "atmel ecc algorithms registered in /proc/crypto\n");
return ret;
}
static void atmel_ecc_remove(struct i2c_client *client)
{
struct atmel_i2c_client_priv *i2c_priv = i2c_get_clientdata(client);
-
- /* Return EBUSY if i2c client already allocated. */
- if (atomic_read(&i2c_priv->tfm_count)) {
- /*
- * After we return here, the memory backing the device is freed.
- * That happens no matter what the return value of this function
- * is because in the Linux device model there is no error
- * handling for unbinding a driver.
- * If there is still some action pending, it probably involves
- * accessing the freed memory.
- */
- dev_emerg(&client->dev, "Device is busy, expect memory corruption.\n");
- return;
- }
-
- crypto_unregister_kpp(&atmel_ecdh_nist_p256);
+ bool trigger_unreg = false;
spin_lock(&driver_data.i2c_list_lock);
list_del(&i2c_priv->i2c_client_list_node);
spin_unlock(&driver_data.i2c_list_lock);
+
+ mutex_lock(&atmel_ecc_kpp_lock);
+ atmel_ecc_kpp_refcnt--;
+ if (atmel_ecc_kpp_refcnt == 0) {
+ trigger_unreg = true;
+ atmel_ecc_unreg_active = true;
+ reinit_completion(&atmel_ecc_unreg_done);
+ }
+ mutex_unlock(&atmel_ecc_kpp_lock);
+
+ if (atomic_read(&i2c_priv->tfm_count))
+ wait_for_completion(&i2c_priv->remove_done);
+
+ if (trigger_unreg) {
+ crypto_unregister_kpp(&atmel_ecdh_nist_p256);
+ mutex_lock(&atmel_ecc_kpp_lock);
+ atmel_ecc_unreg_active = false;
+ complete_all(&atmel_ecc_unreg_done);
+ mutex_unlock(&atmel_ecc_kpp_lock);
+ }
}
static const struct of_device_id atmel_ecc_dt_ids[] = {
diff --git a/drivers/crypto/atmel-i2c.h b/drivers/crypto/atmel-i2c.h
index 72f04c15682f..d957c2ff60d8 100644
--- a/drivers/crypto/atmel-i2c.h
+++ b/drivers/crypto/atmel-i2c.h
@@ -145,6 +145,7 @@ struct atmel_i2c_client_priv {
size_t wake_token_sz;
atomic_t tfm_count ____cacheline_aligned;
struct hwrng hwrng;
+ struct completion remove_done;
};
/**
base-commit: 5624ea54f3ba5c83d2e5503411a31a8be0278c1e
--
2.53.0
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-06-06 20:11 UTC | newest]
Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-06 20:11 [PATCH v2 1/1] crypto: atmel-ecc - fix multi-device use-after-free and registration races Lothar Rubusch
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox