On 2026/06/15 19:59, Daniel P. Berrangé wrote:
On Mon, Jun 15, 2026 at 01:11:07PM +0900, Akihiko Odaki wrote:
One concrete case is qemu-xhci: XHCIPciState embeds XHCIState as a
child object, and the embedded XHCIState owns the MemoryRegion used for
PCI BAR 0. When memory core references that region, memory_region_ref()
references the embedded XHCIState owner, but that does not keep the
containing XHCIPciState storage alive across runtime unplug.

Taking this example and looking at the patch,

Immediately after XHCIPciState is allocated, we'll have the
following XHCIPciState QOM state:

    free == object_free
    storage == <undefined>
    ref == 1
    storage_ref == 1   <up-ref from XHCIState>

It should be:

    ref == 0
    storage_ref == 1

Please see "[PATCH 1/2] qom: Reject temporary object resurrection" for the context. `Object::ref` stores the logical reference count minus one, so `ref == 0` means one reference remains.

`storage_ref` follows the same convention for non-embedded storage. Here, `storage_ref == 1` means two storage references: the storage's own lifetime reference, plus the reference held via the embedded XHCIState.


Meanwhile XHCIState is embedded and so after construction it
has

    free == <undefined>
    storage == ptr<XHCIPciState>
    ref == 1
    storage_ref == UINT32_MAX

`ref == 0` here too.



The simple scenario, no 3rd party refs on XHCIState, then we
have a flow that looks like this:

   object_unref(XHCIPciState)
    => ref == 0

This makes `ref == UINT32_MAX`; that marks the object as being finalized.

        => object_finalize(XHCIPciState)
            => object_unref(XHCIState)
               => ref == 0

This also makes `XHCIState.ref == UINT32_MAX`.

               => object_storage_unref(XHCIState)
                    => obj = obj.storage
                    => XHCIPciState.storage_ref--
                    => XHCIPciState.storage_ref == 0

Yes. At this point there is still one storage reference left, represented
as `storage_ref == 0`: the storage's own lifetime reference, held by the
XHCIPciState finalization path itself.

                     => object_free(XHCIPciState)

`obj->free()` does not happen when `XHCIPciState.storage_ref == 0`.

        => Remainder of ...object_finalize(XHCIPciState) is
           using free'd mem now...

The free happens only after `object_finalize(XHCIPciState)` reaches its
own final `object_storage_unref(XHCIPciState)`. That decrements
`XHCIPciState.storage_ref` from `0` to `UINT32_MAX`, and only then calls
`obj->free()`.


This doesn't seem to work AFAICS ?

Now lets consider something took a reference on XHCIState
first.

   object_ref(XHCIState)
   object_unref(XHCIPciState)
    => ref == 0
        => object_finalize(XHCIPciState)
            => object_unref(XHCIState)
               => ref == 1
        => object_storage_unref(XHCIPciState)
           => storage_ref--
           => storage_ref == 0
               => object_free(XHCIPciState)

   object_unref(XHCIState)
    => ref == 0
    => object_storage_unref(XHCIPciState)
       => storage_ref == -1

Again this doesn't seem to actually work as billed ?

With the minus-one representation, this becomes:

    object_ref(XHCIState)
      => XHCIState.ref goes from 0 to 1

    object_unref(XHCIPciState)
      => XHCIPciState.ref goes from 0 to UINT32_MAX
      => object_finalize(XHCIPciState)
          => object_unref(XHCIState)
             => XHCIState.ref goes from 1 to 0
             => XHCIState is not finalized yet
          => object_storage_unref(XHCIPciState)
             => XHCIPciState.storage_ref goes from 1 to 0
             => no free yet

    object_unref(XHCIState)
      => XHCIState.ref goes from 0 to UINT32_MAX
      => object_finalize(XHCIState)
          => object_storage_unref(XHCIState)
             => follows XHCIState.storage to XHCIPciState
             => XHCIPciState.storage_ref goes from 0 to UINT32_MAX
             => object_free(XHCIPciState)

So the containing storage is not freed until both finalizers have
completed.



For the sake of argument though, lets say this storage_ref
was handled correctly such that we delay free'ing the
XHCIPciState memory  until *both*  XHCIPciState and
XHCIState have hit ref == 0, and both their finalize
methods are completed.

I'm still not convinced this would actually be safe.

This patch is delaying the free'ing of the memory that
XHCIState is embedded in, but is the embedded XHCIState
still functional enough when its parent and all sibling
objects will have been finalized already ? That is not
obviously ok to me. The "XHCIPciState" will have been
finalized, but all its fields are mostly pointing to
data that has been deinitialized. Can we really assume
there are no dependencies between the embedded objects ?

That concern is valid. Keeping the containing storage allocated does not by itself guarantee that the embedded object remains fully functional after its parent has started finalization. That is also why the Rust wrapper for `object_ref()` remains unsafe.

I think this is orthogonal to the storage lifetime problem this patch solves. The same functional-lifetime problem can exist even if XHCIState is not embedded and is allocated with `object_new()`, because it may still depend on state owned by the parent or by sibling objects.

This patch prevents freeing the memory backing an externally referenced embedded object. It does not claim that every object implementation is safe to operate after the parent has begun unrealize/finalization.



Conceptually it feels like finalization of XHCIPciState
ought to be an all-or-nothing affair, rather than
finalizing XHCIPciState and some of its embedded children
but arbtirarily keeping other children alive. I'm not
sure how to arrange that though - its essentially a need
for a circular reference, but then how/when do you break
it.

I agree that all-or-nothing finalization is attractive conceptually, but I do not think it gives enough semantics once objects can refer to each other. We still need to define an ordering: for example, if a parent and an embedded child refer to each other, either the child is finalized first and must not be accessed by the parent afterwards, or the parent is finalized first and the child must not access the parent afterwards.

So the practical rule cannot just be "finalize the aggregate as one unit". We need explicit ordering and state transitions. In the case where the parent is finalized before an externally referenced child, the child may remain allocated, but it must treat the parent-side state as gone and avoid accessing it.

In the MemoryRegion case described in the commit message, one possible higher-level fix is exactly this kind of ordering rule: once the device is being unrealized, accesses through the device's memory regions should be forbidden or should fail gracefully. The MemoryRegion object may remain referenced for a while, but late accesses should not rely on the parent device still being operational.

Regards,
Akihiko Odaki

Reply via email to