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

John Ogness
2025-11-06T18:58:05Z

On 2025-11-06, Petr Mladek <pmladek@suse.com> wrote:
>> 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.

I explicitly chose a word other than "less" or "lower" because I was
concerned people might visualize values. The lpos does not necessarily
have a lesser or lower value. "Preceeds" would also be a choice of mine.

When I see "lt" I immediately think "less than" and "<". But I will not
fight it. I can handle "lt".

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

I find "_safe" confusing. Especially when you look at the implementation
you wonder, "what is safe about this?". Especially when comparing it to
all the complexity of the rest of the code. But I can handle "_safe" if
it is important for you.

> 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.

This makes quite a bit of assumptions about the context and intention of
the call. I preferred my brain teaser version. But to me it is not worth
bike-shedding. If this explanation helps you, I am fine with it.

> */
> 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.

You have overlooked that I inverted the check. It is no longer checking:

next_pos <= head_pos

but is instead checking:

!(head_pos < next_pos)

IOW, if "next has not overtaken head".

> 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 you negate lpos1_lt_lpos2_safe() and swap the parameters, there is no
need for a second helper. That is what I did.

>> @@ -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.

No, we would _not_ expect that zero size is OK, because we are detecting
"Regular data blocks", in which case they must _not_ be equal.

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

Currently we have 3 explicit checks:

1. data-less

2. regular

3. wrapping

But I agree the checks are "relaxed" because we are doing only minimal
sanity checks on the positions, rather than size validation.

> 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.

Currently we do not do size validation for reading, only for writing. If
you are arguing that we _should_ perform better size validation on read,
then I agree this is the place for it.

>> 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?

I think it can be made even more simple since we are adding size
validation:

diff --git a/kernel/printk/printk_ringbuffer.c b/kernel/printk/printk_ringbuffer.c
index b7ab4e75917f0..04bc863eae411 100644
--- a/kernel/printk/printk_ringbuffer.c
+++ b/kernel/printk/printk_ringbuffer.c
@@ -1271,23 +1271,15 @@ 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) {
- db = to_block(data_ring, blk_lpos->begin);
- *data_size = blk_lpos->next - blk_lpos->begin;
-
- /* Wrapping data block: @begin is one wrap behind @next. */
- } else if (!is_blk_wrapped(data_ring,
- blk_lpos->begin + DATA_SIZE(data_ring),
- blk_lpos->next)) {
+ /* Wrapping data block description. */
+ if (is_blk_wrapped(data_ring, blk_lpos->begin, blk_lpos->next)) {
db = to_block(data_ring, 0);
*data_size = DATA_INDEX(data_ring, blk_lpos->next);

- /* Illegal block description. */
+ /* Regular data block description. */
} else {
- WARN_ON_ONCE(1);
- return NULL;
+ db = to_block(data_ring, blk_lpos->begin);
+ *data_size = blk_lpos->next - blk_lpos->begin;
}

/* A valid data block will always be aligned to the ID size. */
@@ -1300,6 +1292,10 @@ static const char *get_data(struct prb_data_ring *data_ring,
if (WARN_ON_ONCE(*data_size < sizeof(db->id)))
return NULL;

+ /* Check if the data size is at least legal. */
+ if (WARN_ON_ONCE(!data_check_size(data_ring, *data_size)))
+ return NULL;
+
/* Subtract block ID space from size to reflect data size. */
*data_size -= sizeof(db->id);

So it ends up looking like this:

/* Wrapping data block description. */
if (is_blk_wrapped(data_ring, blk_lpos->begin, blk_lpos->next)) {
db = to_block(data_ring, 0);
*data_size = DATA_INDEX(data_ring, blk_lpos->next);

/* Regular data block description. */
} else {
db = to_block(data_ring, blk_lpos->begin);
*data_size = blk_lpos->next - blk_lpos->begin;
}
...
/* Ensure the data size is at least legal. */
if (WARN_ON_ONCE(!data_check_size(data_ring, *data_size)))
return NULL;

(Note that there is already WARN_ON_ONCE() checks for misaligned lpos
values and sizes less than sizeof(id).)

How does this sound?

John