Re: [syzbot] [iomap?] kernel BUG in folio_end_read (2)

Petr Mladek
2025-11-06T16:23:00Z

On Thu 2025-11-06 12:42:21, John Ogness wrote:
> On 2025-11-05, John Ogness <john.ogness@linutronix.de> wrote:
> >> Another question is whether this is the only problem caused the patch.
> >
> > This comparison is quite special. It caught my attention while combing
> > through the code.
>
> The reason that this comparison is special is because it is the only one
> that does not take wrapping into account. I did it that way originally
> because it is AND with a wrap check. But this is an ugly special
> case. It should use the same wrap check as the other 3 cases in
> nbcon.c. If it had, the bug would not have happened.

I think that there are actually some differences between the
comparsions, see below.

> I always considered these wrap checks to be non-obvious and
> error-prone. So what if we create a nice helper function to simplify and
> unify the wrap checks? Something like this:

But I agree that some wrappers with a good description
would be helpful.

> diff --git a/kernel/printk/printk_ringbuffer.c b/kernel/printk/printk_ringbuffer.c
> index 839f504db6d30..8499ee642c31d 100644
> --- a/kernel/printk/printk_ringbuffer.c
> +++ b/kernel/printk/printk_ringbuffer.c
> @@ -390,6 +390,17 @@ static unsigned int to_blk_size(unsigned int size)
> return size;
> }
>
> +/*
> + * Check if @lpos1 is before @lpos2. This takes ringbuffer wrapping
> + * into account. If @lpos1 is more than a full wrap before @lpos2,
> + * it is considered to be after @lpos2.

The 2nd sentence is a brain teaser ;-)

> + */
> +static bool lpos1_before_lpos2(struct prb_data_ring *data_ring,
> + unsigned long lpos1, unsigned long lpos2)
> +{
> + return lpos2 - lpos1 - 1 < DATA_SIZE(data_ring);
> +}

It would be nice to describe the semantic a more clean way. Sigh,
it is not easy. I tried several variants and ended up with:

+ using "lt" instead of "before" because "lower than" is
a well known mathematical therm.

+ adding "_safe" suffix to make it clear that it is not
a simple mathematical comparsion. It takes the wrap
into account.

Something like:

/*
* Returns true when @lpos1 is lower than @lpos2 and both values
* are comparable.
*
* It is safe when the compared values are read a lock less way.
* One of them must be already overwritten when the difference
* is bigger then the data ring buffer size.
*/
static bool lpos1_lt_lpos2_safe(struct prb_data_ring *data_ring,
unsined long lpos1, unsigned long lpos2)
{
return lpos2 - lpos1 - 1 < DATA_SIZE(data_ring);
}

> /*
> * Sanity checker for reserve size. The ringbuffer code assumes that a data
> * block does not exceed the maximum possible size that could fit within the
> @@ -577,7 +588,7 @@ static bool data_make_reusable(struct printk_ringbuffer *rb,
> unsigned long id;
>
> /* Loop until @lpos_begin has advanced to or beyond @lpos_end. */
> - while ((lpos_end - lpos_begin) - 1 < DATA_SIZE(data_ring)) {
> + while (lpos1_before_lpos2(data_ring, lpos_begin, lpos_end)) {

lpos1_lt_lpos2_safe() fits here.

> blk = to_block(data_ring, lpos_begin);
> /*
> @@ -668,7 +679,7 @@ static bool data_push_tail(struct printk_ringbuffer *rb, unsigned long lpos)
> * sees the new tail lpos, any descriptor states that transitioned to
> * the reusable state must already be visible.
> */
> - while ((lpos - tail_lpos) - 1 < DATA_SIZE(data_ring)) {
> + while (lpos1_before_lpos2(data_ring, tail_lpos, lpos)) {
> /*
> * Make all descriptors reusable that are associated with
> * data blocks before @lpos.

Same here.

> @@ -1149,7 +1160,7 @@ static char *data_realloc(struct printk_ringbuffer *rb, unsigned int size,
> next_lpos = get_next_lpos(data_ring, blk_lpos->begin, size);
>
> /* If the data block does not increase, there is nothing to do. */
> - if (head_lpos - next_lpos < DATA_SIZE(data_ring)) {
> + if (!lpos1_before_lpos2(data_ring, head_lpos, next_lpos)) {

I think that the original code was correct. And using the "-1" is
wrong here.

Both data_make_reusable() and data_push_tail() had to use "-1"
because it was the "lower than" semantic. But in this case,
we do not need to do anything even when "head_lpos == next_lpos"

By other words, both data_make_reusable() and data_push_tail()
needed to make a free space when the position was "lower than".
There was enough space when the values were "equal".

It means that "equal" should be OK in data_realloc(). By other
words, data_realloc() should use "le" aka "less or equal"
semantic.

The helper function might be:

/*
* Returns true when @lpos1 is lower or equal than @lpos2 and both
* values are comparable.
*
* It is safe when the compared values are read a lock less way.
* One of them must be already overwritten when the difference
* is bigger then the data ring buffer size.
*/
static bool lpos1_le_lpos2_safe(struct prb_data_ring *data_ring,
unsined long lpos1, unsigned long lpos2)
{
return lpos2 - lpos1 < DATA_SIZE(data_ring);
}

> if (wrapped)
> blk = to_block(data_ring, 0);
> else
> @@ -1262,7 +1273,7 @@ static const char *get_data(struct prb_data_ring *data_ring,
>
> /* Regular data block: @begin less than @next and in same wrap. */
> if (!is_blk_wrapped(data_ring, blk_lpos->begin, blk_lpos->next) &&
> - blk_lpos->begin < blk_lpos->next) {
> + lpos1_before_lpos2(data_ring, blk_lpos->begin, blk_lpos->next)) {

Hmm, I think that it is more complicated here.

The "lower than" semantic is weird here. One would expect that "equal"
values, aka "zero size" is perfectly fine.

It does not hurt because the "zero size" case is already handled
earlier. But still, the "lower than" semantic does not fit here.

IMHO, the main motivation for this fix is to make sure that
blk_lpos->begin and blk_lpos->next will produce a valid
*data_size.

From this POV, even lpos1_le_lpos2_safe() does not fit here
because the data_size must be lower than half of the size
of the ring buffer.

> db = to_block(data_ring, blk_lpos->begin);
> *data_size = blk_lpos->next - blk_lpos->begin;

I think that we should do the following:

diff --git a/kernel/printk/printk_ringbuffer.c b/kernel/printk/printk_ringbuffer.c
index 839f504db6d3..78e02711872e 100644
--- a/kernel/printk/printk_ringbuffer.c
+++ b/kernel/printk/printk_ringbuffer.c
@@ -1260,9 +1260,8 @@ static const char *get_data(struct prb_data_ring *data_ring,
return NULL;
}

- /* Regular data block: @begin less than @next and in same wrap. */
- if (!is_blk_wrapped(data_ring, blk_lpos->begin, blk_lpos->next) &&
- blk_lpos->begin < blk_lpos->next) {
+ /* Regular data block: @begin and @next in same wrap. */
+ if (!is_blk_wrapped(data_ring, blk_lpos->begin, blk_lpos->next)) {
db = to_block(data_ring, blk_lpos->begin);
*data_size = blk_lpos->next - blk_lpos->begin;

@@ -1279,6 +1278,10 @@ static const char *get_data(struct prb_data_ring *data_ring,
return NULL;
}

+ /* Double check that the data_size is reasonable. */
+ if (WARN_ON_ONCE(!data_check_size(data_ring, *data_size)))
+ return NULL;
+
/* A valid data block will always be aligned to the ID size. */
if (WARN_ON_ONCE(blk_lpos->begin != ALIGN(blk_lpos->begin, sizeof(db->id))) ||
WARN_ON_ONCE(blk_lpos->next != ALIGN(blk_lpos->next, sizeof(db->id)))) {

1. Just distinguish regular vs. wrapped. vs. invalid block.

2. Add sanity check for the "data_size". It might catch some wrong values
in both code paths for "regular" and "wrapped" blocks. So, win win.

How does that sound?

Best Regards,
Petr