Re: [RFC PATCH v2 1/8] block: associate blkg in submit_bio instead of bio_set_dev

"yu kuai" <[email protected]> Wed, 29 Jul 2026 15:49:41 +0800
Newsgroups org.kernel.vger.cgroups,org.kernel.vger.linux-block
Message-ID <[email protected]>
Hi,

在 2026/7/29 15:23, Tao Cui 写道:
>
> 在 2026/7/24 20:30, Yu Kuai 写道:
>> From: Yu Kuai <[email protected]>
>>
>> bio_set_dev(), bio_init() and bio_reset() associate a bio with a blkg for
>> its target queue. That association may have to create a new blkg, and
>> currently there are lots of callers that are under atomic context.
>>
>> Move the association out of those helpers and into the submit path, which
>> is always sleepable (submit_bio_noacct() already does might_sleep()):
>>
>>    - submit_bio() associates new I/O before bio_set_ioprio(), whose
>>      blkcg_set_ioprio() reads the policy from bio->bi_blkg.
>>
>>    - submit_bio_noacct() (re)associates when a bio has no blkg yet or was
>>      remapped to a different queue.  blk_throtl_bio() and the rq_qos
>>      throttlers (iocost, iolatency) pair bio->bi_blkg with the queue of
>>      bio->bi_bdev, so a remapped bio must be reassociated to the new queue.
>>
>>    - bio_set_dev() no longer associates; instead it drops the existing blkg
>>      when the device changes, since that blkg is tied to the old queue.
>>      bio_init()/bio_reset() leave bi_blkg NULL.
>>
> Hi Yu Kuai,
>
> Thanks for the v2 rework.  One thing I wanted to flag: with association
> moved out of bio_set_dev(), blkcg_punt_bio_submit() can be reached with
> bio->bi_blkg == NULL.  btrfs compressed writeback builds its bio without
> going through the generic submit_bio() (it uses btrfs_submit_bio()), so the
> new association never runs for it, and the bio_set_dev() in
> btrfs_submit_dev_bio() no longer fills bi_blkg in.
>
> I reproduced it on this tree -- btrfs with compress=zstd, write dirty data
> from a non-root cgroup, sync:
>
>    BUG: kernel NULL pointer dereference, address 0x30
>    Workqueue: btrfs-worker btrfs_work_helper
>    RIP: 0010:blkcg_punt_bio_submit+0xf/0x80
>    Call Trace:
>     btrfs_submit_bio+0x79/0x1a0
>     btrfs_work_helper+0x141/0x360
>
> 0x30 is the offset of ->parent in struct blkcg_gq, and RBX (bio->bi_blkg)
> is NULL at the fault, so it's the `if (blkg->parent)` read.  Regular
> buffered writeback looked fine -- wbc_init_bio() sets bi_blkg early -- so it
> seems limited to the compressed path.

Thanks for the tests. Yes, I already know the problem, and replied a few days
ago. :)

>
>> Introduce bio_disassociate_blkg() to drop a bio's blkg reference and use it
>> from bio_set_dev(), bio_uninit() and the bio freeing path, replacing their
>> open-coded blkg_put().
>>
>> This is the first step to convert protecting blkg with blkcg_mutex.
>>
>> Signed-off-by: Yu Kuai <[email protected]>
>> ---
>>   block/bio.c         | 18 ++----------------
>>   block/blk-cgroup.c  | 18 ++++++++++++++++++
>>   block/blk-core.c    | 21 +++++++++++++++++++++
>>   include/linux/bio.h |  7 +++++--
>>   4 files changed, 46 insertions(+), 18 deletions(-)
>>
>> diff --git a/block/bio.c b/block/bio.c
>> index 6a2f6fc3413e..ea030c5e39a4 100644
>> --- a/block/bio.c
>> +++ b/block/bio.c
>> @@ -179,12 +179,7 @@ static inline gfp_t try_alloc_gfp(gfp_t gfp)
>>   
>>   void bio_uninit(struct bio *bio)
>>   {
>> -#ifdef CONFIG_BLK_CGROUP
>> -	if (bio->bi_blkg) {
>> -		blkg_put(bio->bi_blkg);
>> -		bio->bi_blkg = NULL;
>> -	}
>> -#endif
>> +	bio_disassociate_blkg(bio);
>>   	if (bio_integrity(bio))
>>   		bio_integrity_free(bio);
>>   
>> @@ -235,8 +230,6 @@ void bio_init(struct bio *bio, struct block_device *bdev, struct bio_vec *table,
>>   #ifdef CONFIG_BLK_CGROUP
>>   	bio->bi_blkg = NULL;
>>   	bio->issue_time_ns = 0;
>> -	if (bdev)
>> -		bio_associate_blkg(bio);
>>   #ifdef CONFIG_BLK_CGROUP_IOCOST
>>   	bio->bi_iocost_cost = 0;
>>   #endif
>> @@ -280,8 +273,6 @@ void bio_reset(struct bio *bio, struct block_device *bdev, blk_opf_t opf)
>>   	atomic_set(&bio->__bi_remaining, 1);
>>   	bio->bi_io_vec = bv;
>>   	bio->bi_bdev = bdev;
>> -	if (bio->bi_bdev)
>> -		bio_associate_blkg(bio);
>>   	bio->bi_opf = opf;
>>   }
>>   EXPORT_SYMBOL(bio_reset);
>> @@ -1803,17 +1794,12 @@ void bio_endio(struct bio *bio)
>>   		goto again;
>>   	}
>>   
>> -#ifdef CONFIG_BLK_CGROUP
>>   	/*
>>   	 * Release cgroup info.  We shouldn't have to do this here, but quite
>>   	 * a few callers of bio_init fail to call bio_uninit, so we cover up
>>   	 * for that here at least for now.
>>   	 */
>> -	if (bio->bi_blkg) {
>> -		blkg_put(bio->bi_blkg);
>> -		bio->bi_blkg = NULL;
>> -	}
>> -#endif
>> +	bio_disassociate_blkg(bio);
>>   
>>   	if (bio->bi_end_io)
>>   		bio->bi_end_io(bio);
>> diff --git a/block/blk-cgroup.c b/block/blk-cgroup.c
>> index d9676126c5b5..618e5566fa52 100644
>> --- a/block/blk-cgroup.c
>> +++ b/block/blk-cgroup.c
>> @@ -2170,6 +2170,24 @@ void bio_clone_blkg_association(struct bio *dst, struct bio *src)
>>   }
>>   EXPORT_SYMBOL_GPL(bio_clone_blkg_association);
>>   
>> +/**
>> + * bio_disassociate_blkg - disassociate a bio from its blkg
>> + * @bio: target bio
>> + *
>> + * Drop the blkg reference held by @bio and clear the association.  This is
>> + * used when a bio's target device changes (e.g. via bio_set_dev()): the old
>> + * blkg is tied to the previous request_queue, so it is dropped here and the
>> + * bio is reassociated to the new queue from the submit path.
>> + */
>> +void bio_disassociate_blkg(struct bio *bio)
>> +{
>> +	if (bio->bi_blkg) {
>> +		blkg_put(bio->bi_blkg);
>> +		bio->bi_blkg = NULL;
>> +	}
>> +}
>> +EXPORT_SYMBOL_GPL(bio_disassociate_blkg);
>> +
>>   static int blk_cgroup_io_type(struct bio *bio)
>>   {
>>   	if (op_is_discard(bio->bi_opf))
>> diff --git a/block/blk-core.c b/block/blk-core.c
>> index 365641266c9e..8103643b39fc 100644
>> --- a/block/blk-core.c
>> +++ b/block/blk-core.c
>> @@ -822,6 +822,20 @@ void submit_bio_noacct(struct bio *bio)
>>   
>>   	might_sleep();
>>   
>> +	/*
>> +	 * Associate the bio with a blkg for its target queue here, where it is
>> +	 * safe to sleep, instead of in bio_set_dev()/bio_init() which may run
>> +	 * under a spinlock.  bio_set_dev() no longer associates, so a freshly
>> +	 * allocated or remapped bio has no blkg until it reaches the submit path.
>> +	 * Reassociate only when the bio was remapped to a different queue since
>> +	 * it was last associated; blk_throtl_bio() and the rq_qos throttlers
>> +	 * rely on bio->bi_blkg matching the queue of bio->bi_bdev.
>> +	 */
>> +#ifdef CONFIG_BLK_CGROUP
>> +	if (!bio->bi_blkg || bio->bi_blkg->q != q)
>> +		bio_associate_blkg(bio);
>> +#endif
>> +
>>   	/*
>>   	 * For a REQ_NOWAIT based request, return -EOPNOTSUPP
>>   	 * if queue does not support NOWAIT.
>> @@ -958,6 +972,13 @@ void submit_bio(struct bio *bio)
>>   		count_vm_events(PGPGOUT, bio_sectors(bio));
>>   	}
>>   
>> +	/*
>> +	 * bio_set_ioprio() -> blkcg_set_ioprio() reads the policy from
>> +	 * bio->bi_blkg, so associate the blkg (for new I/O, the first time) before
>> +	 * it runs.  This is the sleepable entry point for new I/O; remapped bios
>> +	 * that reach submit_bio_noacct() directly are reassociated there.
>> +	 */
>> +	bio_associate_blkg(bio);
>>   	bio_set_ioprio(bio);
>>   	submit_bio_noacct(bio);
>>   }
>> diff --git a/include/linux/bio.h b/include/linux/bio.h
>> index 8f33f717b14f..7a7509c8a59a 100644
>> --- a/include/linux/bio.h
>> +++ b/include/linux/bio.h
>> @@ -507,6 +507,7 @@ void bio_associate_blkg(struct bio *bio);
>>   void bio_associate_blkg_from_css(struct bio *bio,
>>   				 struct cgroup_subsys_state *css);
>>   void bio_clone_blkg_association(struct bio *dst, struct bio *src);
>> +void bio_disassociate_blkg(struct bio *bio);
>>   void blkcg_punt_bio_submit(struct bio *bio);
>>   #else	/* CONFIG_BLK_CGROUP */
>>   static inline void bio_associate_blkg(struct bio *bio) { }
>> @@ -515,6 +516,7 @@ static inline void bio_associate_blkg_from_css(struct bio *bio,
>>   { }
>>   static inline void bio_clone_blkg_association(struct bio *dst,
>>   					      struct bio *src) { }
>> +static inline void bio_disassociate_blkg(struct bio *bio) { }
>>   static inline void blkcg_punt_bio_submit(struct bio *bio)
>>   {
>>   	submit_bio(bio);
>> @@ -524,10 +526,11 @@ static inline void blkcg_punt_bio_submit(struct bio *bio)
>>   static inline void bio_set_dev(struct bio *bio, struct block_device *bdev)
>>   {
>>   	bio_clear_flag(bio, BIO_REMAPPED);
>> -	if (bio->bi_bdev != bdev)
>> +	if (bio->bi_bdev != bdev) {
>>   		bio_clear_flag(bio, BIO_BPS_THROTTLED);
>> +		bio_disassociate_blkg(bio);
>> +	}
>>   	bio->bi_bdev = bdev;
>> -	bio_associate_blkg(bio);
>>   }
>>   
>>   /*

-- 
Thanks,
Kuai