During testing of the preceding changes, I noticed that in some cases,
current->kcsan_ctx.in_flat_atomic remained true until task exit. This is
obviously wrong, because _all_ accesses for the given task will be
treated as atomic, resulting in false negatives i.e. missed data races.
Debugging led to fs/dcache.c, where we can see this usage of seqlock:
struct dentry *d_lookup(const struct dentry *parent, const struct qstr *name)
{
struct dentry *dentry;
unsigned seq;
do {
seq = read_seqbegin(&rename_lock);
dentry = __d_lookup(parent, name);
if (dentry)
break;
} while (read_seqretry(&rename_lock, seq));
[...]
As can be seen, read_seqretry() is never called if dentry != NULL;
consequently, current->kcsan_ctx.in_flat_atomic will never be reset to
false by read_seqretry().
Give up on the wrong assumption of "assume closing read_seqretry()", and
rely on the already-present annotations in read_seqcount_begin/retry().
Fixes: 88ecd153be95 ("seqlock, kcsan: Add annotations for KCSAN")
Signed-off-by: Marco Elver <elver@google.com>
---
v2:
* New patch.
---
include/linux/seqlock.h | 12 +-----------
1 file changed, 1 insertion(+), 11 deletions(-)
diff --git a/include/linux/seqlock.h b/include/linux/seqlock.h
index 45eee0e5dca0..5298765d6ca4 100644
--- a/include/linux/seqlock.h
+++ b/include/linux/seqlock.h
@@ -810,11 +810,7 @@ static __always_inline void write_seqcount_latch_end(seqcount_latch_t *s)
*/
static inline unsigned read_seqbegin(const seqlock_t *sl)
{
- unsigned ret = read_seqcount_begin(&sl->seqcount);
-
- kcsan_atomic_next(0); /* non-raw usage, assume closing read_seqretry() */
- kcsan_flat_atomic_begin();
- return ret;
+ return read_seqcount_begin(&sl->seqcount);
}
/**
@@ -830,12 +826,6 @@ static inline unsigned read_seqbegin(const seqlock_t *sl)
*/
static inline unsigned read_seqretry(const seqlock_t *sl, unsigned start)
{
- /*
- * Assume not nested: read_seqretry() may be called multiple times when
- * completing read critical section.
- */
- kcsan_flat_atomic_end();
-
return read_seqcount_retry(&sl->seqcount, start);
}
--
2.47.0.163.g1226f6d8fa-goog
On Mon, Nov 04, 2024 at 04:43:09PM +0100, Marco Elver wrote: > During testing of the preceding changes, I noticed that in some cases, > current->kcsan_ctx.in_flat_atomic remained true until task exit. This is > obviously wrong, because _all_ accesses for the given task will be > treated as atomic, resulting in false negatives i.e. missed data races. > > Debugging led to fs/dcache.c, where we can see this usage of seqlock: > > struct dentry *d_lookup(const struct dentry *parent, const struct qstr *name) > { > struct dentry *dentry; > unsigned seq; > > do { > seq = read_seqbegin(&rename_lock); > dentry = __d_lookup(parent, name); > if (dentry) > break; > } while (read_seqretry(&rename_lock, seq)); > [...] How's something like this completely untested hack? struct dentry *dentry; read_seqcount_scope (&rename_lock) { dentry = __d_lookup(parent, name); if (dentry) break; } But perhaps naming isn't right, s/_scope/_loop/ ? --- a/include/linux/seqlock.h +++ b/include/linux/seqlock.h @@ -829,6 +829,33 @@ static inline unsigned read_seqretry(con return read_seqcount_retry(&sl->seqcount, start); } + +static inline unsigned read_seq_scope_begin(const struct seqlock_t *sl) +{ + unsigned ret = read_seqcount_begin(&sl->seqcount); + kcsan_atomic_next(0); + kcsan_flat_atomic_begin(); + return ret; +} + +static inline void read_seq_scope_end(unsigned *seq) +{ + kcsan_flat_atomic_end(); +} + +static inline bool read_seq_scope_retry(const struct seqlock_t *sl, unsigned *seq) +{ + bool done = !read_seqcount_retry(&sl->seqcount, *seq); + if (!done) + *seq = read_seqcount_begin(&sl->seqcount); + return done; +} + +#define read_seqcount_scope(sl) \ + for (unsigned seq __cleanup(read_seq_scope_end) = \ + read_seq_scope_begin(sl), done = 0; \ + !done; done = read_seq_scope_retry(sl, &seq)) + /* * For all seqlock_t write side functions, use the internal * do_write_seqcount_begin() instead of generic write_seqcount_begin().
On Tue, 5 Nov 2024 at 10:34, Peter Zijlstra <peterz@infradead.org> wrote: > > On Mon, Nov 04, 2024 at 04:43:09PM +0100, Marco Elver wrote: > > During testing of the preceding changes, I noticed that in some cases, > > current->kcsan_ctx.in_flat_atomic remained true until task exit. This is > > obviously wrong, because _all_ accesses for the given task will be > > treated as atomic, resulting in false negatives i.e. missed data races. > > > > Debugging led to fs/dcache.c, where we can see this usage of seqlock: > > > > struct dentry *d_lookup(const struct dentry *parent, const struct qstr *name) > > { > > struct dentry *dentry; > > unsigned seq; > > > > do { > > seq = read_seqbegin(&rename_lock); > > dentry = __d_lookup(parent, name); > > if (dentry) > > break; > > } while (read_seqretry(&rename_lock, seq)); > > [...] > > > How's something like this completely untested hack? > > > struct dentry *dentry; > > read_seqcount_scope (&rename_lock) { > dentry = __d_lookup(parent, name); > if (dentry) > break; > } > > > But perhaps naming isn't right, s/_scope/_loop/ ? _loop seems straightforward. > --- a/include/linux/seqlock.h > +++ b/include/linux/seqlock.h > @@ -829,6 +829,33 @@ static inline unsigned read_seqretry(con > return read_seqcount_retry(&sl->seqcount, start); > } > > + > +static inline unsigned read_seq_scope_begin(const struct seqlock_t *sl) > +{ > + unsigned ret = read_seqcount_begin(&sl->seqcount); > + kcsan_atomic_next(0); > + kcsan_flat_atomic_begin(); > + return ret; > +} > + > +static inline void read_seq_scope_end(unsigned *seq) > +{ > + kcsan_flat_atomic_end(); If we are guaranteed to always have one _begin paired by a matching _end, we can s/kcsan_flat_atomic/kcsan_nestable_atomic/ for these. > +} > + > +static inline bool read_seq_scope_retry(const struct seqlock_t *sl, unsigned *seq) > +{ > + bool done = !read_seqcount_retry(&sl->seqcount, *seq); > + if (!done) > + *seq = read_seqcount_begin(&sl->seqcount); > + return done; > +} > + > +#define read_seqcount_scope(sl) \ > + for (unsigned seq __cleanup(read_seq_scope_end) = \ > + read_seq_scope_begin(sl), done = 0; \ > + !done; done = read_seq_scope_retry(sl, &seq)) > + That's nice! I think before we fully moved over to C11, I recall Mark and I discussed something like that (on IRC?) but gave up until C11 landed and then we forgot. ;-)
On Tue, Nov 05, 2024 at 10:34:00AM +0100, Peter Zijlstra wrote: > On Mon, Nov 04, 2024 at 04:43:09PM +0100, Marco Elver wrote: > > During testing of the preceding changes, I noticed that in some cases, > > current->kcsan_ctx.in_flat_atomic remained true until task exit. This is > > obviously wrong, because _all_ accesses for the given task will be > > treated as atomic, resulting in false negatives i.e. missed data races. > > > > Debugging led to fs/dcache.c, where we can see this usage of seqlock: > > > > struct dentry *d_lookup(const struct dentry *parent, const struct qstr *name) > > { > > struct dentry *dentry; > > unsigned seq; > > > > do { > > seq = read_seqbegin(&rename_lock); > > dentry = __d_lookup(parent, name); > > if (dentry) > > break; > > } while (read_seqretry(&rename_lock, seq)); > > [...] > > > How's something like this completely untested hack? > > > struct dentry *dentry; > > read_seqcount_scope (&rename_lock) { > dentry = __d_lookup(parent, name); > if (dentry) > break; > } > > > But perhaps naming isn't right, s/_scope/_loop/ ? It is also confused between seqcount and seqlock. So perhaps it should read: read_seqcount_loop (&rename_lock.seqcount) { ... } instead.
On Mon, Nov 04, 2024 at 04:43:09PM +0100, Marco Elver wrote: > During testing of the preceding changes, I noticed that in some cases, > current->kcsan_ctx.in_flat_atomic remained true until task exit. This is > obviously wrong, because _all_ accesses for the given task will be > treated as atomic, resulting in false negatives i.e. missed data races. > > Debugging led to fs/dcache.c, where we can see this usage of seqlock: > > struct dentry *d_lookup(const struct dentry *parent, const struct qstr *name) > { > struct dentry *dentry; > unsigned seq; > > do { > seq = read_seqbegin(&rename_lock); > dentry = __d_lookup(parent, name); > if (dentry) > break; > } while (read_seqretry(&rename_lock, seq)); > [...] > > As can be seen, read_seqretry() is never called if dentry != NULL; > consequently, current->kcsan_ctx.in_flat_atomic will never be reset to > false by read_seqretry(). > > Give up on the wrong assumption of "assume closing read_seqretry()", and > rely on the already-present annotations in read_seqcount_begin/retry(). > > Fixes: 88ecd153be95 ("seqlock, kcsan: Add annotations for KCSAN") > Signed-off-by: Marco Elver <elver@google.com> > --- > v2: > * New patch. > --- > include/linux/seqlock.h | 12 +----------- > 1 file changed, 1 insertion(+), 11 deletions(-) > > diff --git a/include/linux/seqlock.h b/include/linux/seqlock.h > index 45eee0e5dca0..5298765d6ca4 100644 > --- a/include/linux/seqlock.h > +++ b/include/linux/seqlock.h > @@ -810,11 +810,7 @@ static __always_inline void write_seqcount_latch_end(seqcount_latch_t *s) > */ > static inline unsigned read_seqbegin(const seqlock_t *sl) > { > - unsigned ret = read_seqcount_begin(&sl->seqcount); > - > - kcsan_atomic_next(0); /* non-raw usage, assume closing read_seqretry() */ > - kcsan_flat_atomic_begin(); > - return ret; > + return read_seqcount_begin(&sl->seqcount); > } > > /** > @@ -830,12 +826,6 @@ static inline unsigned read_seqbegin(const seqlock_t *sl) > */ > static inline unsigned read_seqretry(const seqlock_t *sl, unsigned start) > { > - /* > - * Assume not nested: read_seqretry() may be called multiple times when > - * completing read critical section. > - */ > - kcsan_flat_atomic_end(); > - > return read_seqcount_retry(&sl->seqcount, start); > } OK, so this takes us back to kcsan_atomic_next(KCSAN_SEQLOCK_REGION_MAX) and kcsan_atomic_next(0). Which I suppose is safe, except it doesn't nest properly. Anyway, these all look really nice, let me go queue them up. Thanks!
On Tue, 5 Nov 2024 at 10:13, Peter Zijlstra <peterz@infradead.org> wrote: > > static inline unsigned read_seqretry(const seqlock_t *sl, unsigned start) > > { > > - /* > > - * Assume not nested: read_seqretry() may be called multiple times when > > - * completing read critical section. > > - */ > > - kcsan_flat_atomic_end(); > > - > > return read_seqcount_retry(&sl->seqcount, start); > > } > > OK, so this takes us back to kcsan_atomic_next(KCSAN_SEQLOCK_REGION_MAX) > and kcsan_atomic_next(0). > > Which I suppose is safe, except it doesn't nest properly. Yes correct - we just give up on trying to be special here. It would be nice to also have readers have a clear critical section, but that seems a lot harder to enforce with a bunch of them having rather convoluted control flow. :-/ > Anyway, these all look really nice, let me go queue them up. Many thanks!
The following commit has been merged into the locking/core branch of tip:
Commit-ID: 183ec5f26b2fc97a4a9871865bfe9b33c41fddb2
Gitweb: https://git.kernel.org/tip/183ec5f26b2fc97a4a9871865bfe9b33c41fddb2
Author: Marco Elver <elver@google.com>
AuthorDate: Mon, 04 Nov 2024 16:43:09 +01:00
Committer: Peter Zijlstra <peterz@infradead.org>
CommitterDate: Tue, 05 Nov 2024 12:55:35 +01:00
kcsan, seqlock: Fix incorrect assumption in read_seqbegin()
During testing of the preceding changes, I noticed that in some cases,
current->kcsan_ctx.in_flat_atomic remained true until task exit. This is
obviously wrong, because _all_ accesses for the given task will be
treated as atomic, resulting in false negatives i.e. missed data races.
Debugging led to fs/dcache.c, where we can see this usage of seqlock:
struct dentry *d_lookup(const struct dentry *parent, const struct qstr *name)
{
struct dentry *dentry;
unsigned seq;
do {
seq = read_seqbegin(&rename_lock);
dentry = __d_lookup(parent, name);
if (dentry)
break;
} while (read_seqretry(&rename_lock, seq));
[...]
As can be seen, read_seqretry() is never called if dentry != NULL;
consequently, current->kcsan_ctx.in_flat_atomic will never be reset to
false by read_seqretry().
Give up on the wrong assumption of "assume closing read_seqretry()", and
rely on the already-present annotations in read_seqcount_begin/retry().
Fixes: 88ecd153be95 ("seqlock, kcsan: Add annotations for KCSAN")
Signed-off-by: Marco Elver <elver@google.com>
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Link: https://lore.kernel.org/r/20241104161910.780003-6-elver@google.com
---
include/linux/seqlock.h | 12 +-----------
1 file changed, 1 insertion(+), 11 deletions(-)
diff --git a/include/linux/seqlock.h b/include/linux/seqlock.h
index 45eee0e..5298765 100644
--- a/include/linux/seqlock.h
+++ b/include/linux/seqlock.h
@@ -810,11 +810,7 @@ static __always_inline void write_seqcount_latch_end(seqcount_latch_t *s)
*/
static inline unsigned read_seqbegin(const seqlock_t *sl)
{
- unsigned ret = read_seqcount_begin(&sl->seqcount);
-
- kcsan_atomic_next(0); /* non-raw usage, assume closing read_seqretry() */
- kcsan_flat_atomic_begin();
- return ret;
+ return read_seqcount_begin(&sl->seqcount);
}
/**
@@ -830,12 +826,6 @@ static inline unsigned read_seqbegin(const seqlock_t *sl)
*/
static inline unsigned read_seqretry(const seqlock_t *sl, unsigned start)
{
- /*
- * Assume not nested: read_seqretry() may be called multiple times when
- * completing read critical section.
- */
- kcsan_flat_atomic_end();
-
return read_seqcount_retry(&sl->seqcount, start);
}
© 2016 - 2024 Red Hat, Inc.