Re: [BUG] WARNING in unlink_anon_vmas()

From: Lorenzo Stoakes (Oracle)

Date: Wed Mar 18 2026 - 07:58:08 EST


+cc Sasha

On Wed, Mar 18, 2026 at 10:59:33AM +0000, Lorenzo Stoakes (Oracle) wrote:
> (-cc old email)
>
> On Wed, Mar 18, 2026 at 06:42:49PM +0800, Jiakai Xu wrote:
> > Hi all,
> >
> > While fuzzing the KVM subsystem on RISC-V, I stumbled upon a kernel WARNING
> > that triggers in unlink_anon_vmas().
>
> Thanks!
>
> Will have a look at this.
>
> >
> > WARNING: mm/rmap.c:528 at unlink_anon_vmas+0x562/0x768 mm/rmap.c:528
> > unlink_anon_vmas+0x562/0x768 mm/rmap.c:528
>
> Assuming there's not some big mismatch with kernel versions this is:
>
> VM_WARN_ON(anon_vma->num_active_vmas);

OK so this _was_ reported by Sasha (via an AI assessment), but it completely
dropped off my radar sorry about that!

https://lore.kernel.org/linux-mm/20260302151547.2389070-1-sashal@xxxxxxxxxx/

I want to fix this a slightly different way though.

SO what's happening is in dup_anon_vma() we do:

static int dup_anon_vma(struct vm_area_struct *dst,
struct vm_area_struct *src, struct vm_area_struct **dup)
{
...

dst->anon_vma = src->anon_vma;
ret = anon_vma_clone(dst, src, VMA_OP_MERGE_UNFAULTED);
if (ret)
return ret; <-- fault injection error here

*dup = dst; <-- NOT set

...
}

Then:

int anon_vma_clone(struct vm_area_struct *dst, struct vm_area_struct *src,
enum vma_operation operation)
{
...

list_for_each_entry(pavc, &src->anon_vma_chain, same_vma) {
avc = anon_vma_chain_alloc(GFP_KERNEL); <-- FAULT INJECTED HERE
if (!avc)
goto enomem_failure; <-- so we do this

anon_vma_chain_assign(dst, avc, pavc->anon_vma);
}

...

if (operation != VMA_OP_FORK)
dst->anon_vma->num_active_vmas++; <-- does NOT get run

...

enomem_failure:
cleanup_partial_anon_vmas(dst);
return -ENOMEM;
}

We only clear down the partially allocated anon_vma_chain objects:

static void cleanup_partial_anon_vmas(struct vm_area_struct *vma)
{
struct anon_vma_chain *avc, *next;

list_for_each_entry_safe(avc, next, &vma->anon_vma_chain, same_vma) {
list_del(&avc->same_vma);
anon_vma_chain_free(avc);
}
}

But, crucially, dst->anon_vma is LEFT IN PLACE.

So on process exit, we call into unlink_anon_vmas() for that VMA:

void unlink_anon_vmas(struct vm_area_struct *vma)
{
...
struct anon_vma *active_anon_vma = vma->anon_vma; <-- is SET

...

/* Unfaulted is a no-op. */
if (!active_anon_vma) { <-- is not called
VM_WARN_ON_ONCE(!list_empty(&vma->anon_vma_chain));
return;
}

...

active_anon_vma->num_active_vmas--; <-- Incorrect (*)

...

list_for_each_entry_safe(avc, next, &vma->anon_vma_chain, same_vma) {
...

VM_WARN_ON(anon_vma->num_active_vmas); <-- triggers
put_anon_vma(anon_vma);

...
}
}

* If anon_vma->num_active_vmas was 0, it underflows, but otherwise it'll get
decrement one time too many, and so will _eventually underflow_ guaranteed and
trigger the bug for for VMAs associated with this anon_vma.

The fix is to set vma->anon_vma = NULL in this situation, which I think is best
done in the cleanup code as I said at
https://lore.kernel.org/linux-mm/a709c736-fd76-4bc9-a1d2-e1351742b321@lucifer.local/
but then... didn't do as it dropped off my radar (oops!)

Will send a fix + cc, attribute Reported-by etc., thanks very much for reporting
this Jiakai, was very useful!

Cheers, Lorenzo