The Linux Kernel Mailing List
 help / color / mirror / Atom feed
From: "Danilo Krummrich" <dakr@kernel.org>
To: "Alan Stern" <stern@rowland.harvard.edu>
Cc: "Oliver Neukum" <oneukum@suse.com>,
	"Colin Braun" <colinbrauncl@gmail.com>,
	"Miguel Ojeda" <ojeda@kernel.org>,
	"Greg Kroah-Hartman" <gregkh@linuxfoundation.org>,
	"Boqun Feng" <boqun@kernel.org>, "Gary Guo" <gary@garyguo.net>,
	"Björn Roy Baron" <bjorn3_gh@protonmail.com>,
	"Benno Lossin" <lossin@kernel.org>,
	"Andreas Hindborg" <a.hindborg@kernel.org>,
	"Alice Ryhl" <aliceryhl@google.com>,
	"Trevor Gross" <tmgross@umich.edu>,
	"Daniel Almeida" <daniel.almeida@collabora.com>,
	"Tamir Duberstein" <tamird@kernel.org>,
	"Alexandre Courbot" <acourbot@nvidia.com>,
	"Onur Özkan" <work@onurozkan.dev>,
	"Mauro Carvalho Chehab" <mchehab@kernel.org>,
	"Mathias Nyman" <mathias.nyman@intel.com>,
	linux-kernel@vger.kernel.org, rust-for-linux@vger.kernel.org,
	linux-usb@vger.kernel.org, linux-media@vger.kernel.org,
	"Colin Braun" <colin.braun.cl@gmail.com>,
	"Rafael J. Wysocki" <rafael@kernel.org>,
	driver-core@lists.linux.dev
Subject: Re: [RFC PATCH 2/4] rust: usb: add usb host interface and endpoint abstractions
Date: Wed, 15 Jul 2026 02:27:08 +0200	[thread overview]
Message-ID: <DJYPVXEKX4I3.LITS0PZKHBJ1@kernel.org> (raw)
In-Reply-To: <a6809829-b7f8-4181-9965-0668fe95d133@rowland.harvard.edu>

On Tue Jul 14, 2026 at 9:25 PM CEST, Alan Stern wrote:
> On Tue, Jul 14, 2026 at 07:53:03PM +0200, Danilo Krummrich wrote:
>> (Cc: driver-core)
>> 
>> On Tue Jul 14, 2026 at 6:26 PM CEST, Alan Stern wrote:
>> > I don't see why Rust needs to distinguish between a USB device that is 
>> > bound and one that is unbound.  There isn't much you can do with one 
>> > that can't be done with the other.
>> 
>> It has nothing to do with Rust, those driver lifecycle rules exist regardless
>> and they are present universally, including in C.
>> 
>> The only difference is that in C all the responsibility to enforce them is
>> usually on the driver -- e.g. by creating, destroying and calling things in the
>> correct order in probe() and remove() -- and a lot of drivers have bugs in this
>> regard as a consequence.
>> 
>> With Rust we can enforce those rules with the help of the type system at compile
>> time; device context states are a part of that.
>
> I don't really understand how that would work.  For example, suppose you 
> have an object whose type represents an unbound USB interface (I forget 
> what you are calling this).  Then a driver is probed and binds to the 
> interface.  What happens to the object?  Is it somehow destroyed and 
> replaced by a new object of a different type, representing a bound 
> interface?  Or does the object remain unchanged but you create a new 
> reference to it, of the new type?

The usb::Interface struct definition looks like this:

	#[repr(transparent)]
	pub struct Interface<Ctx: device::DeviceContext = device::Normal>(
	    Opaque<bindings::usb_interface>,
	    PhantomData<Ctx>,
	);

It is a transparent wrapper around a struct usb_interface, which means it is
layout compatible, i.e. in memory is has the exact same representation as struct
usb_interface.

The default type state is "Normal", so usb::Interface actually means
usb::Interface<Normal>, which means "a USB interface without any additional
guarantees".

When we receive a pointer of a struct usb_interface from C, we can (unsafely)
create any transparent wrapper type of it as a "Rust reference" [1], e.g.

	extern "C" fn probe_callback(
	    intf: *mut bindings::usb_interface,
	    id: *const bindings::usb_device_id,
	) -> kernel::ffi::c_int {
	    // `intf` is of type `&usb::Interface`.
	    let intf =  unsafe { &*intf.cast::<usb::Interface>() };
	
	    // `intf` is of type `&usb::Interface<Bound>`.
	    let intf =  unsafe { &*intf.cast::<usb::Interface<Bound>>() };
	
	    // `intf` is of type `&usb::Interface<Core<'_>>`.
	    let intf =  unsafe { &*intf.cast::<usb::Interface<Core<'_>>>() };

	    ...
	}

In memory they are all identical, but for the compiler they now carry different
type information.

When we now e.g. call the driver's Rust probe() function, we pass the struct
usb_interface pointer as &'bound usb::Interface<Core<'_>>.

The 'Core' type state only lives for the duration of probe() and indicates that
we are in a bus callback where the device lock of struct usb_interface is held,
which means that we can implement methods that require the device lock of struct
usb_interface to be held for usb::Interface<Core> only, so they are not
available for e.g. usb::Interface or usb::Interface<Bound>.

&'bound usb::Interface<Core<'_>> dereferences (see [2]) to &'bound
usb::Interface<Bound>, as the 'Core' type state implies 'Bound'.

Let's have a look at &'bound usb::Interface<Bound> as it becomes relevant later
on.

The '&' indicates a reference [1], which can be roughly described as a pointer
with additional guarantees that are tracked by the compiler. One of those is the
lifetime of the pointer, which in my example is explicitly named as 'bound.

[1] https://doc.rust-lang.org/reference/types/pointer.html#references--and-mut
[2] https://doc.rust-lang.org/std/ops/trait.Deref.html

> Also, what happens while the binding or unbinding procedure is underway, 
> so the interface is, so to speak, partially bound?  The USB stack does 
> actually take notice of this; see the definition of enum 
> usb_interface_condition in include/linux/usb.h.

The driver core doesn't make this distinction in the direction of driver APIs.
The 'Bound' type state goes from call_driver_probe() until device_remove() has
finished.

The exact point doesn't really matter to drivers, for them 'Bound' begins with
their probe() callback and ends with the destructor of their bus device private
data.

The enum usb_interface_condition seems to me like an approach to synchronize the
initialization and teardown sequence of interfaces in regards to their parent
device within the USB core code, but it isn't driver facing.

>> > Similarly, I don't see why Rust needs to distinguish between an 
>> > interface that is bound and one that isn't.
>> >
>> > Even from the point of view of the device core, a device that is bound 
>> > to a driver is the same kind of data structure as one that isn't bound; 
>> > the only difference is whether the ->driver pointer is set.
>> 
>> This is a huge understatement.
>> 
>> The state of a device being bound to a driver defines which entity (i.e. which
>> driver) is in charge of operating the underlying device, and thus defines who
>> owns the device (associated) resources.
>
> That's not how I would describe it.  When a device is bound to a driver, 
> the driver is allowed to create and use associated resource; when the 
> device is not bound, no such resources should exist.

I think we are basically saying the same thing; no driver bound to the device
means no one "owns" the device and hence no one operates it or manages (its)
resources.

>> Many APIs rely on this, as in they only guarantee valid behavior when called
>> from a scope where the device is guaranteed to be bound to a driver, or IOW
>> where a driver can prove that it actually operates the device.
>
> I can't think of many APIs like that in the USB stack.  One that springs 
> to mind is encapsulated by checkintf() and check_ctrlrecip() in 
> core/devio.c, but those are the exception rather than the rule.

It's not only the USB specific APIs, a usb::Interface<Bound> would also give you
the underlying, embedded struct device as Device<Bound>, which you need for
various APIs to operate.

For instance, requesting DMA coherent memory that has IOMMU mappings requires a
Device<Bound>, the same goes for SGTable mappings.

Another example are class device registrations and workqueues. I will give an
example how that looks like in Rust, but you can think of it similar to APIs
like devm_work_autocancel() and devm_iio_device_register().

Those APIs require the device embedding the core struct device to be bound to a
driver (and so does the Rust Devres API).

For USB specifically, you want to require the 'Bound' device state for anything
that operates the device in a way that only a driver should do that is, well,
bound to the device. For instance, any methods that perform I/O operations (e.g.
usb_submit_urb(), which AFAIK implies DMA mappings too) with the device should
be implemented for the 'Bound' type state only.

IOW, drivers should be restricted from having DMA mappings for a device and
performing I/O with the device outside the scope between probe() and remove().

>> Drivers must only acquire device resources when they are actually bound to the
>> corresponding device, and must hand them back before the device is unbound. The
>> devres API, for instance, exists for this fundamental reason.
>
> How would having separate types for bound and unbound interfaces enable 
> Rust to recognize that a driver had not destroyed back a resource in its 
> unbind callback?

The abstract answer is that together with the Rust compiler's ownership and
lifetime tracking we can represent relationships such as "A lives at least as
long as B", "A does not outlive B", etc.

I will sketch up a concrete example below.

>> For instance, we can't have drivers manage IRQs, mess with I/O memory, program
>> IOMMU page tables (e.g. through DMA APIs), etc. for devices they are not bound
>> to and hence are not allowed to operate (anymore).
>> 
>> Those device resources all have a lifetime that is tied to the lifetime of the
>> device being bound to a driver.
>> 
>> Consequently, any asynchronous scopes such as IOCTLs from class device
>> registrations, IRQs, work queued on workqueues, etc. must all be synchronized in
>> some way such that those asynchronous scopes do not access device resources that
>> have already been destroyed on driver unbind.
>
> That is certainly true.  But I don't see how it can be enforced at 
> compile time.

(I'll try to explain the mechanics below; a lot of this requires deep
familiarity with Rust's ownership and lifetime system, so I'll try to keep it
approachable while still being precise enough to be useful. Also note that I'm
simplifying a few details for the same reason. As mentioned, I will also try to
sketch up a concrete example driver below.)

Let's assume we are in probe() of a Rust driver, then we will eventually have a

	&'bound usb::Interface<Bound>

i.e. a reference to a bound USB interface with the lifetime 'bound.

When we now try to create a class device registration, let's say a DRM device
registration, it looks like this:

	// `dev` is the embedded `struct device` as `&'bound Device<Bound>`.
	let dev = intf.as_ref();

	// `drm`:  The DRM device.
	// `data`: Private data that lives as long as the DRM device is
	//         registered.
	let reg = drm::Registration::new(dev, drm, data)?

What happens is that `reg` captures the lifetime of `dev`, i.e. 'bound and the
compiler will now ensure that `reg` can't outlive 'bound.

The same goes for the private data `data`; it also captures this lifetime, so
you can store a &'bound usb::Interface<Bound> within `data`.

Now, we finally need to store `reg` somewhere before probe() ends, otherwise it
will go out of scope and be destroyed right away, which would be useless.

The only place where it can go is the bus device private data, i.e. the private
data of the usb::Interface.

Technically, this is returned as an initializer by probe(), but that's just an
implementation detail. The relevant part is that since we control the signature
of probe(), i.e.

	fn probe<'bound>(
	    adev: &'bound auxiliary::Device<Core<'_>>,
	    _info: &'bound Self::IdInfo,
	) -> impl PinInit<Self::Data<'bound>, Error> + 'bound {
	    ...
	}

we can define that the return value captures this lifetime too. So, this is the
only place where this data can go, and since the driver core controls the
lifetime of the returned private data, the driver core will ensure that it is
destroyed when the driver is unbound.

When we now receive an IOCTL from the DRM class device you will get your `data`
back, which contains your original &'bound usb::Interface<Bound>.

And the reason you can get your &'bound usb::Interface<Bound> is because the
drm::Registration ties itself to the 'bound lifetime and guarantees that once it
is destroyed no more IOCTLs can hit the driver.

You can now apply the same pattern to any other registration kind of thing.
IRQs, workqueues, work items, etc.

Of course there are also primitives that do not tie their lifetime to the 'bound
lifetime and just exist as long as a driver wants them to exist, but those can't
capture any types that capture the 'bound lifetime themselves.

For instance, the compiler will not allow you to pass a

	&'bound usb::Interface<Bound>

to

	workqueue::system().enqueue();

instead you'd need to pass it to a Rust equivalent of devm_work_autocancel() for
the compiler to accept it.

>> Or in other words, they must be synchronized against the "bound" scope, which is
>> exactly what the Device<Bound> type state in Rust represents.
>> 
>> So, again, all those lifetime rules around the driver lifecycle exist
>> universally, it's just that in Rust we enforce them through the type system.
>
> How do you enforce through the type system that, for example, a 
> workqueue item has completed?

Like I mentioned above, if you look for the code, please see [1]. But the trick
is the same, it captures the 'bound lifetime and the destructor, which calls
destroy_workqueue() which will wait for pending work.

[1] https://lore.kernel.org/all/20260617144645.253444-2-work@onurozkan.dev/

>> For instance, tying it back to USB, we don't want that a usb_driver still messes
>> with a usb_interface, e.g. initiating transfers after it has been unbound from
>> the interface and hence must not operate it anymore. This can easily happen if
>> e.g. a class device registration is not properly synchronized and the driver
>> still receives IOCTLs after driver unbind. In Rust we know through the "Bound"
>> type state which scope provides the guarantee that the device is still bound,
>> such that mistakes like this become impossible.
>
> How can you pass types around to different control threads with somewhat 
> arbitrary synchronization schemes managed at runtime, all while not 
> allowing a typed reference to exist beyond its lifetime?

Here's the example I promised, to keep it simple I will just use
platform::Device, MMIO and an IRQ handler, but the concept applies universally.

Please see the inline comments for additional explanation.

Note that in addition the approach does also remove the typical ordering
pitfalls we have in C driver's probe() functions by modelling them as real data
dependencies the compiler can actually check.

```
const REGS_SIZE: usize = 0x1000;

// Common infrastructure to define types for registers.
register! {
    STATUS(u32) @ 0x00 {
        31:0 pending;
    }

    ACK(u32) @ 0x04 {
        31:0 clear;
    }

    CONTROL(u32) @ 0x08 {
        0:0 enable;
        1:1 irq_enable;
    }
}

// Custom impl block for the `ACK` register.
impl ACK {
    fn from_status(status: STATUS) -> Self {
        Self::from(status.pending().get())
    }
}

// The IRQ handler, which captures the `'bound` lifetime. We use `'irq` as a
// name to indicate that it might live shorter, but in any case it can't live
// longer than `'bound`, which is ensured by the `irq::Registration`
// constructor.
//
// The only thing we store in the `irq::Registration` is the `iomem`, so we can
// perform I/O. Note that `ExclusiveIoMem` also captures a lifetime as a device
// resource, i.e. it can't outlive `'bound` either.
struct IrqHandler<'irq> {
    iomem: &'irq ExclusiveIoMem<'irq, REGS_SIZE>,
    // Not used for anything other than printing, but you get the idea.
    pdev: &'irq platform::Device<Bound>,
}

// The implementation of the IRQ handler.
impl irq::Handler for IrqHandler<'_> {
    fn handle(&self) -> IrqReturn {
        let status = self.iomem.read(STATUS);

        if status.pending() == 0 {
            return IrqReturn::None;
        }
        self.iomem.write_reg(ACK::from_status(status));

        dev_info!(self.pdev, "I must not print from hard IRQs.\n");
        IrqReturn::Handled
    }
}

// The bus device private data structure, which captures the `'bound` lifetime.
// It lives exactly as long as the driver is bound to the device.
//
// Both `iomem` and the IRQ registration `_irq` live until the driver is
// unbound from the device.
//
// When `irq::Registration` is destroyed no more IRQs can fire.
#[pin_data(PinnedDrop)]
struct SampleData<'bound> {
    #[pin]
    _irq: irq::Registration<'bound, IrqHandler<'bound>>,
    iomem: ExclusiveIoMem<'bound, REGS_SIZE>,
}

// The `Drop` implementation (destructor) of the bus device private data.
//
// This is pretty much equivalent to remove(); all resources such as `iomem`
// and `_irq` are cleaned up automatically through their `Drop` implementation.
#[pinned_drop]
impl PinnedDrop for SampleData<'_> {
    fn drop(self: Pin<&mut Self>) {
        self.iomem.write_reg(CONTROL::zeroed());
    }
}

struct SampleDriver;

// The implementation of the platform::Driver, some type information, ID table,
// bus callbacks, etc.
impl platform::Driver for SampleDriver {
    // The type of the device ID data; we don't have any hence the unit type
    // (think of it as void).
    type IdInfo = ();

    // The type of the driver's bus device private data.
    type Data<'bound> = SampleData<'bound>;

    // ID tables, we don't have any in this example.
    const OF_ID_TABLE: Option<kernel::of::IdTable<Self::IdInfo>> = None;
    const ACPI_ID_TABLE: Option<kernel::acpi::IdTable<Self::IdInfo>> = None;

    // The probe() function. The pin-init syntax may be a bit confusing, but
    // going into pin-init details is a bit out of scope.
    //
    // Think of it as something that captures a recipe of how memory of a
    // certain type (`SampleData` in this case) has to be initialized to be
    // valid.
    fn probe<'bound>(
        pdev: &'bound platform::Device<Core<'_>>,
        _info: Option<&'bound Self::IdInfo>,
    ) -> impl PinInit<Self::Data<'bound>, Error> + 'bound {
        // We create and return an initializer for `SampleData`.
        try_pin_init!(SampleData {
            // We initialize the first field `iomem`; the `platform::Device`
            // provides a method on `platform::Device<Bound>` which `pdev`
            // dereferences to as it has the `Core` device context.
            //
            // iomap_exclusive_sized() captures the lifetime of `pdev` and
            // stores it in `iomem`. The only place this can live in is probe()
            // and this initializer; the driver core takes care that the
            // constructed `SampleData` can't outlive `'bound`.
            iomem: pdev
                .io_request_by_index(0)
                .ok_or(ENODEV)?
                .iomap_exclusive_sized::<REGS_SIZE>()?,

            // Similar to `iomem`, request_irq_by_index() requires a
            // `platform::Device<Bound>` and captures the `'bound` lifetime for
            // both `IrqHandler` and the returned `irq::Registration`.
            //
            // Note that the `IrqHandler` stores a reference to `iomem`, this is
            // only possible as pin-init will take care that the field has
            // actually been initialized previously and is dropped after the
            // `IrqHandler` is dropped.
            //
            // It becomes impossible to get the order wrong and accidentally
            // register the IRQ before `iomem`; it can't happen that an IRQ
            // fires before `iomem` is initialized.
            //
            // The reason I use future tense is because pin-init doesn't do that
            // yet; people are working on it. There are other options to do this
            // safely (e.g. with reference counts, etc.), but this is the one
            // with zero overhead we are heading towards.
            _irq <- pdev.request_irq_by_index(
                        Flags::SHARED,
                        0,
                        c"sample-irq",
                        IrqHandler { iomem: &iomem, pdev: pdev }),

            // This initializes absolutely nothing, but pin-init allows us to
            // execute arbitrary code in our recipe. So, now that everything
            // else is initialized, enable the controller.
            _: {
                let ctrl = CONTROL::zeroed().with_enable(true).with_irq_enable(true);
                iomem.write_reg(ctrl);
            },
        })
    }
}

kernel::module_platform_driver! {
    type: SampleDriver,
    name: "rust_driver_sample",
    authors: ["Danilo Krummrich"],
    description: "Sample Driver",
    license: "GPL v2",
}
```

I also plan to give a talk about this at Kernel Recipes [1] where I go into
details and try to give some comparison between C and Rust code.

[1] https://kernel-recipes.org/en/2026/schedule/enforcing-device-driver-lifecycle-rules-at-compile-time/

  reply	other threads:[~2026-07-15  0:27 UTC|newest]

Thread overview: 28+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-12 21:07 [RFC PATCH 0/4] rust: usb: add usb request block abstractions and a user Colin Braun
2026-07-12 21:07 ` [RFC PATCH 1/4] rust: usb: add USB ch9 standard descriptors and constants Colin Braun
2026-07-12 21:07 ` [RFC PATCH 2/4] rust: usb: add usb host interface and endpoint abstractions Colin Braun
2026-07-13 13:22   ` Danilo Krummrich
2026-07-13 20:03     ` Colin Braun
2026-07-13 20:09       ` Danilo Krummrich
2026-07-14  9:26         ` Oliver Neukum
2026-07-14 13:05           ` Danilo Krummrich
2026-07-14 16:26             ` Alan Stern
2026-07-14 17:53               ` Danilo Krummrich
2026-07-14 18:48                 ` Oliver Neukum
2026-07-14 18:57                   ` Danilo Krummrich
2026-07-14 19:25                 ` Alan Stern
2026-07-15  0:27                   ` Danilo Krummrich [this message]
2026-07-14 17:57               ` Oliver Neukum
2026-07-14 19:03                 ` Alan Stern
2026-07-14 18:26               ` Miguel Ojeda
2026-07-12 21:08 ` [RFC PATCH 3/4] rust: usb: add urb abstraction with control and isochronous support Colin Braun
2026-07-12 21:08 ` [RFC PATCH 4/4] media: add gv-usb2 audio capture driver Colin Braun
2026-07-13 14:44   ` Danilo Krummrich
2026-07-13 21:08     ` Colin Braun
2026-07-13 13:22 ` [RFC PATCH 0/4] rust: usb: add usb request block abstractions and a user Danilo Krummrich
2026-07-13 20:10   ` Colin Braun
2026-07-13 13:53 ` Daniel Almeida
2026-07-13 20:32   ` Colin Braun
2026-07-15  2:35     ` Daniel Almeida
2026-07-15  5:43       ` Greg Kroah-Hartman
2026-07-15 19:57       ` Colin Braun

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=DJYPVXEKX4I3.LITS0PZKHBJ1@kernel.org \
    --to=dakr@kernel.org \
    --cc=a.hindborg@kernel.org \
    --cc=acourbot@nvidia.com \
    --cc=aliceryhl@google.com \
    --cc=bjorn3_gh@protonmail.com \
    --cc=boqun@kernel.org \
    --cc=colin.braun.cl@gmail.com \
    --cc=colinbrauncl@gmail.com \
    --cc=daniel.almeida@collabora.com \
    --cc=driver-core@lists.linux.dev \
    --cc=gary@garyguo.net \
    --cc=gregkh@linuxfoundation.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-media@vger.kernel.org \
    --cc=linux-usb@vger.kernel.org \
    --cc=lossin@kernel.org \
    --cc=mathias.nyman@intel.com \
    --cc=mchehab@kernel.org \
    --cc=ojeda@kernel.org \
    --cc=oneukum@suse.com \
    --cc=rafael@kernel.org \
    --cc=rust-for-linux@vger.kernel.org \
    --cc=stern@rowland.harvard.edu \
    --cc=tamird@kernel.org \
    --cc=tmgross@umich.edu \
    --cc=work@onurozkan.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox