Revocable Resource Management¶
Overview¶
The “revocable” mechanism is a synchronization primitive designed to manage safe access to resources that can be asynchronously removed or invalidated. Its primary purpose is to prevent Use-After-Free (UAF) errors when interacting with resources whose lifetimes are not guaranteed to outlast their consumers.
This is particularly useful in systems where resources can disappear unexpectedly, such as those provided by hot-pluggable devices like USB. When a consumer holds a reference to such a resource, the underlying device might be removed, causing the resource’s memory to be freed. Subsequent access attempts by the consumer would then lead to UAF errors.
Revocable addresses this by providing a form of “weak reference” and a controlled access method. It allows a resource consumer to safely attempt to access the resource. The mechanism guarantees that any access granted is valid for the duration of its use. If the resource has already been revoked (i.e., freed), the access attempt will fail safely, typically by returning NULL, instead of causing a crash.
The implementation uses a provider/consumer model built on Sleepable RCU (SRCU) to guarantee safe memory access:
A resource provider, such as a driver for a hot-pluggable device, allocates a
struct revocable_providerand initializes it with a pointer to the resource.A resource consumer that wants to access the resource allocates a
struct revocablewhich acts as a handle containing a reference to the provider.To access the resource, the consumer uses
revocable_try_access(). This function enters an SRCU read-side critical section and returns the pointer to the resource. If the provider has already freed the resource, it returns NULL. After use, the consumer callsrevocable_withdraw_access()to exit the SRCU critical section. TheREVOCABLE_TRY_ACCESS_WITH()andREVOCABLE_TRY_ACCESS_SCOPED()are convenient helpers for doing that.When the provider needs to remove the resource, it calls
revocable_provider_revoke(). This function sets the internal resource pointer to NULL and then callssynchronize_srcu()to wait for all current readers to finish before the resource can be completely torn down.
Revocable vs. Devres (devm)¶
It’s important to understand the distinct roles of the Revocable and Devres, and how they can complement each other. They address different problems in resource management:
Devres: Primarily address resource leaks. The lifetime of the resources is tied to the lifetime of the device. The resource is automatically freed when the device is unbound. This cleanup happens irrespective of any potential active users.
Revocable: Primarily addresses invalid memory access, such as Use-After-Free (UAF). It’s an independent synchronization primitive that decouples consumer access from the resource’s actual presence. Consumers interact with a “revocable object” (an intermediary), not the underlying resource directly. This revocable object persists as long as there are active references to it from consumer handles.
Key Distinctions & How They Complement Each Other:
Reference Target: Consumers of a resource managed by the Revocable mechanism hold a reference to the revocable object, not the encapsulated resource itself.
Resource Lifetime vs. Access: The underlying resource’s lifetime is independent of the number of references to the revocable object. The resource can be freed at any point. A common scenario is the resource being freed by devres when the providing device is unbound.
Safe Access: Revocable provides a safe way to attempt access. Before using the resource, a consumer uses the Revocable API (e.g.,
revocable_try_access()). This function checks if the resource is still valid. It returns a pointer to the resource only if it hasn’t been revoked; otherwise, it returns NULL. This prevents UAF by providing a clear signal that the resource is gone.Complementary Usage: devres and Revocable work well together. devres can handle the automatic allocation and deallocation of a resource tied to a device. The Revocable mechanism can be layered on top to provide safe access for consumers whose lifetimes might extend beyond the provider device’s lifetime. For instance, a userspace program might keep a character device file open even after the physical device has been removed. In this case:
devres frees the device-specific resource upon unbinding.
The Revocable mechanism ensures that any subsequent operations on the open file handle, which attempt to access the now-freed resource, will fail gracefully (e.g.,
revocable_try_access()returns NULL) instead of causing a UAF.
In summary, devres ensures resources are released to prevent leaks, while the Revocable mechanism ensures that attempts to access these resources are done safely, even if the resource has been released.
API and Usage¶
For Resource Providers¶
-
struct revocable_provider¶
A handle for resource provider.
Definition:
struct revocable_provider {
struct srcu_struct srcu;
void *res;
struct kref kref;
};
Members
srcuThe SRCU to protect the resource.
resThe pointer of resource. It can point to anything.
krefThe refcount for this handle.
-
struct revocable_provider *revocable_provider_alloc(void *res)¶
Allocate
struct revocable_provider.
Parameters
void *resThe pointer of resource.
Description
This holds an initial refcount to the struct.
Return
The pointer of struct revocable_provider. NULL on errors.
-
struct revocable_provider *devm_revocable_provider_alloc(struct device *dev, void *res)¶
Dev-managed
revocable_provider_alloc().
Parameters
struct device *devThe device.
void *resThe pointer of resource.
Description
It is convenient to allocate providers via this function if the res is
also tied to the lifetime of the dev. revocable_provider_revoke() will
be called automatically when the device is unbound.
This holds an initial refcount to the struct.
Return
The pointer of struct revocable_provider. NULL on errors.
-
void revocable_provider_revoke(struct revocable_provider *rp)¶
Revoke the managed resource.
Parameters
struct revocable_provider *rpThe pointer of resource provider.
Description
This sets the resource (struct revocable_provider *)->res to NULL to
indicate the resource has gone.
This drops the refcount to the resource provider. If it is the final
reference, revocable_provider_release() will be called to free the struct.
For Resource Consumers¶
-
struct revocable¶
A handle for resource consumer.
Definition:
struct revocable {
struct revocable_provider *rp;
int idx;
};
Members
rpThe pointer of resource provider.
idxThe index for the RCU critical section.
-
struct revocable *revocable_alloc(struct revocable_provider *rp)¶
Allocate
struct revocable.
Parameters
struct revocable_provider *rpThe pointer of resource provider.
Description
This holds a refcount to the resource provider.
Return
The pointer of struct revocable. NULL on errors.
-
void revocable_free(struct revocable *rev)¶
Free
struct revocable.
Parameters
struct revocable *revThe pointer of
struct revocable.
Description
This drops a refcount to the resource provider. If it is the final
reference, revocable_provider_release() will be called to free the struct.
Parameters
struct revocable *revThe pointer of
struct revocable.
Description
This tries to de-reference to the resource and enters a RCU critical section.
Return
The pointer to the resource. NULL if the resource has gone.
Parameters
struct revocable *revThe pointer of
struct revocable.
Description
Call this function to indicate the resource is no longer used. It exits the RCU critical section.
-
REVOCABLE_TRY_ACCESS_WITH¶
REVOCABLE_TRY_ACCESS_WITH (_rev, _res)
A helper for accessing revocable resource
Parameters
_revThe consumer’s
struct revocable *handle._resA pointer variable that will be assigned the resource.
Description
The macro simplifies the access-release cycle for consumers, ensuring that
revocable_withdraw_access() is always called, even in the case of an early
exit.
It creates a local variable in the current scope. _res is populated with
the result of revocable_try_access(). The consumer code must check if
_res is NULL before using it. The revocable_withdraw_access() function
is automatically called when the scope is exited.
Note
It shares the same issue with guard() in cleanup.h. No goto statements
are allowed before the helper. Otherwise, the compiler fails with
“jump bypasses initialization of variable with __attribute__((cleanup))”.
Example Usage¶
void consumer_use_resource(struct revocable *rev)
{
struct foo_resource *res;
REVOCABLE_TRY_ACCESS_WITH(rev, res);
// Always check if the resource is valid.
if (!res) {
pr_warn("Resource is not available\n");
return;
}
// At this point, 'res' is guaranteed to be valid until
// this block exits.
do_something_with(res);
} // revocable_withdraw_access() is automatically called here.
-
REVOCABLE_TRY_ACCESS_SCOPED¶
REVOCABLE_TRY_ACCESS_SCOPED (_rev, _res)
A helper for accessing revocable resource
Parameters
_revThe consumer’s
struct revocable *handle._resA pointer variable that will be assigned the resource.
Description
Similar to REVOCABLE_TRY_ACCESS_WITH() but with an explicit scope from a
temporary for loop.
Example Usage¶
void consumer_use_resource(struct revocable *rev)
{
struct foo_resource *res;
REVOCABLE_TRY_ACCESS_SCOPED(rev, res) {
// Always check if the resource is valid.
if (!res) {
pr_warn("Resource is not available\n");
return;
}
// At this point, 'res' is guaranteed to be valid until
// this block exits.
do_something_with(res);
}
// revocable_withdraw_access() is automatically called here.
}