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_provider and initializes it with a pointer to the resource.

  • A resource consumer that wants to access the resource allocates a struct revocable which 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 calls revocable_withdraw_access() to exit the SRCU critical section. The REVOCABLE_TRY_ACCESS_WITH() and REVOCABLE_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 calls synchronize_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:

  1. Reference Target: Consumers of a resource managed by the Revocable mechanism hold a reference to the revocable object, not the encapsulated resource itself.

  2. 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.

  3. 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.

  4. 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

srcu

The SRCU to protect the resource.

res

The pointer of resource. It can point to anything.

kref

The refcount for this handle.

struct revocable_provider *revocable_provider_alloc(void *res)

Allocate struct revocable_provider.

Parameters

void *res

The 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 *dev

The device.

void *res

The 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 *rp

The 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

rp

The pointer of resource provider.

idx

The index for the RCU critical section.

struct revocable *revocable_alloc(struct revocable_provider *rp)

Allocate struct revocable.

Parameters

struct revocable_provider *rp

The 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 *rev

The 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.

void *revocable_try_access(struct revocable *rev)

Try to access the resource.

Parameters

struct revocable *rev

The 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.

void revocable_withdraw_access(struct revocable *rev)

Stop accessing to the resource.

Parameters

struct revocable *rev

The 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

_rev

The consumer’s struct revocable * handle.

_res

A 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

_rev

The consumer’s struct revocable * handle.

_res

A 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.
}