Re: [BUG] fanotify: destroy/add race leaves a mark on a detached connector

From: Amir Goldstein

Date: Thu Aug 27 2026 - 09:00:59 EST


On Thu, Aug 27, 2026 at 6:50 AM Daehyeon Ko <4ncienth@xxxxxxxxx> wrote:
>
> Hello,
>
> I found an unprivileged race in fsnotify_destroy_marks() that can leave an
> attached fanotify mark on a detached connector. On unmodified Linux v7.2 and
> v6.12.105 source builds, it naturally reaches the fsnotify_conn_mask()
> warning from fanotify_mark(). On unmodified builds, the only observed
> system-wide failure was a v7.2 panic when panic_on_warn=1; no memory
> corruption or privilege escalation was observed.
>
> Tested and source-inspected versions:
>
> - Linux v7.2, commit
> 8d3ae59288f1e7d58d76558a6ee96d533bc5019f: reproduced.
> - Linux v6.12.105, commit
> 14c37ff05f22da2fa7076d10f6a07c7ede330c83: reproduced.
> - Torvalds master at
> 73e3f0710014fe6d4ed98cfc02292f6121db7558: the relevant destroy
> iterator and fdinfo consumer remain present. Commit e422777fdd47
> changed mark-mask updates and can mask the immediate warning, but it
> did not change this iterator or the detached-connector state.
>
> The unconditional connector detachment involved in the race appears to
> originate in commit 6b3f05d24d35 ("fsnotify: Detach mark from object list
> when last reference is dropped"), released in v4.12-rc1. My unprivileged
> runtime results are limited to the two versions listed above. The limited
> unprivileged fanotify functionality used by this reproducer was introduced
> by 7cea2a3c505e ("fanotify: support limited functionality for unprivileged
> users"), released in v5.13-rc1; I am not asserting ordinary-user
> reachability before that change.
>
> Root cause:
>
> fsnotify_destroy_marks() holds a reference to the current mark, drops
> conn->lock, destroys that mark, and then continues the hlist walk. While the
> lock is dropped, an equal-priority mark can be inserted immediately before
> the current entry. The walk resumes from the current entry's next pointer,
> so it never visits the new predecessor, but it still detaches the connector
> from the object after the walk.
>
> The skipped mark remains ALIVE|ATTACHED and remains on both its group list
> and the connector list. At the same time, the object no longer points to the
> connector, conn->obj is NULL, and conn->type is DETACHED.
>
> Representative v6.12.105 output is:
>
> WARNING: CPU: 1 PID: 163 at fs/notify/mark.c:128
> fsnotify_conn_mask+0x113/0x150
> CPU: 1 UID: 65534 PID: 163 Comm: fanotify_destro
> ...
> do_fanotify_mark
> __x64_sys_fanotify_mark
>
> The source reproducer ran on ext4 as UID/GID 65534 with CapEff=0 and
> NoNewPrivs=1. It uses ordinary FAN_REPORT_FID groups and races final unlink
> against fd-based mark insertion. Both builds used a KASAN-enabled research
> configuration, but neither positive was a KASAN report.
>
> With panic_on_warn=0 and panic_on_oops=0, the unmodified v7.2 run emitted the
> warning and then completed 10,000 iterations and powered off cleanly. A
> separate fresh v7.2 run with panic_on_warn=1 reached the same warning and
> panicked through check_panic_on_warn(). The v6.12.105 run emitted seven
> warnings and reached its 10,000-iteration progress line; it later timed out
> during post-loop teardown, for which I am not assigning a kernel cause.
>
> A timing-only diagnostic kernel also demonstrated that fdinfo can combine
> the old INODE type with the detached NULL object and reach
> fanotify_show_fdinfo() -> igrab(NULL). I have not reproduced that Oops on an
> unmodified kernel, so it is not part of the impact claim. The skipped mark's
> group-list reference keeps the connector allocated; I have no evidence of a
> connector UAF, arbitrary read or write, information disclosure, or LPE.
>
> I do not have a submission-ready patch. During fix development I rejected a
> survivor-preservation prototype because it introduced a mask race, a
> drain/restart prototype because additions could starve cleanup, and a bounded
> retry prototype because review found an unresolved lock-order/deadlock risk
> when generic callers hold external locks needed by cleanup. I am reporting
> the root cause rather than sending an unsafe fix.
>
> A tested source reproducer, full serial logs, configs, and diagnostic details
> are available privately to the maintainers on request. AI-assisted tooling
> was used during discovery and analysis; I reviewed the source and the literal
> runtime evidence above and am reporting publicly without the reproducer as
> required by Documentation/process/security-bugs.rst.
>

Hi Daehyeon,

Thanks for the detailed report.

Without getting into discussions on solutions to the race, do we even care
that a detached connector has a leftover attached mark in this tiny corner case?

I mean the assertion was added because we did not anticipate it,
but now that we understand that it can happen, the assertion could be removed
or relaxed if there are no other major consequences.

Regarding the reproduced igrab(NULL), this could be fixed by
using a helper like this in show fdino code:

static inline struct inode *fsnotify_conn_get_inode_safe(struct
fsnotify_mark_connector *conn)
{
struct inode *inode = conn->obj;

return inode ? igrab(inode) : NULL;
}

WDYT?

Thanks,
Amir.