From: Greg Kroah-Hartman Date: Thu, 15 Jan 2026 10:26:51 +0000 (+0100) Subject: 6.18-stable patches X-Git-Tag: v6.6.121~26 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f95924d0a17b940fe60fac4c6a105beefb464379;p=thirdparty%2Fkernel%2Fstable-queue.git 6.18-stable patches added patches: btrfs-fix-beyond-eof-write-handling.patch btrfs-truncate-ordered-extent-when-skipping-writeback-past-i_size.patch btrfs-use-variable-for-end-offset-in-extent_writepage_io.patch gpio-mpsse-add-quirk-support.patch gpio-mpsse-ensure-worker-is-torn-down.patch gpio-mpsse-fix-reference-leak-in-gpio_mpsse_probe-error-paths.patch --- diff --git a/queue-6.18/btrfs-fix-beyond-eof-write-handling.patch b/queue-6.18/btrfs-fix-beyond-eof-write-handling.patch new file mode 100644 index 0000000000..c02dbb2003 --- /dev/null +++ b/queue-6.18/btrfs-fix-beyond-eof-write-handling.patch @@ -0,0 +1,155 @@ +From stable+bounces-208120-greg=kroah.com@vger.kernel.org Mon Jan 12 14:55:01 2026 +From: Sasha Levin +Date: Mon, 12 Jan 2026 08:54:51 -0500 +Subject: btrfs: fix beyond-EOF write handling +To: stable@vger.kernel.org +Cc: Qu Wenruo , Filipe Manana , David Sterba , Sasha Levin +Message-ID: <20260112135451.704777-3-sashal@kernel.org> + +From: Qu Wenruo + +[ Upstream commit e9e3b22ddfa760762b696ac6417c8d6edd182e49 ] + +[BUG] +For the following write sequence with 64K page size and 4K fs block size, +it will lead to file extent items to be inserted without any data +checksum: + + mkfs.btrfs -s 4k -f $dev > /dev/null + mount $dev $mnt + xfs_io -f -c "pwrite 0 16k" -c "pwrite 32k 4k" -c pwrite "60k 64K" \ + -c "truncate 16k" $mnt/foobar + umount $mnt + +This will result the following 2 file extent items to be inserted (extra +trace point added to insert_ordered_extent_file_extent()): + + btrfs_finish_one_ordered: root=5 ino=257 file_off=61440 num_bytes=4096 csum_bytes=0 + btrfs_finish_one_ordered: root=5 ino=257 file_off=0 num_bytes=16384 csum_bytes=16384 + +Note for file offset 60K, we're inserting a file extent without any +data checksum. + +Also note that range [32K, 36K) didn't reach +insert_ordered_extent_file_extent(), which is the correct behavior as +that OE is fully truncated, should not result any file extent. + +Although file extent at 60K will be later dropped by btrfs_truncate(), +if the transaction got committed after file extent inserted but before +the file extent dropping, we will have a small window where we have a +file extent beyond EOF and without any data checksum. + +That will cause "btrfs check" to report error. + +[CAUSE] +The sequence happens like this: + +- Buffered write dirtied the page cache and updated isize + + Now the inode size is 64K, with the following page cache layout: + + 0 16K 32K 48K 64K + |/////////////| |//| |//| + +- Truncate the inode to 16K + Which will trigger writeback through: + + btrfs_setsize() + |- truncate_setsize() + | Now the inode size is set to 16K + | + |- btrfs_truncate() + |- btrfs_wait_ordered_range() for [16K, u64(-1)] + |- btrfs_fdatawrite_range() for [16K, u64(-1)} + |- extent_writepage() for folio 0 + |- writepage_delalloc() + | Generated OE for [0, 16K), [32K, 36K] and [60K, 64K) + | + |- extent_writepage_io() + + Then inside extent_writepage_io(), the dirty fs blocks are handled + differently: + + - Submit write for range [0, 16K) + As they are still inside the inode size (16K). + + - Mark OE [32K, 36K) as truncated + Since we only call btrfs_lookup_first_ordered_range() once, which + returned the first OE after file offset 16K. + + - Mark all OEs inside range [16K, 64K) as finished + Which will mark OE ranges [32K, 36K) and [60K, 64K) as finished. + + For OE [32K, 36K) since it's already marked as truncated, and its + truncated length is 0, no file extent will be inserted. + + For OE [60K, 64K) it has never been submitted thus has no data + checksum, and we insert the file extent as usual. + This is the root cause of file extent at 60K to be inserted without + any data checksum. + + - Clear dirty flags for range [16K, 64K) + It is the function btrfs_folio_clear_dirty() which searches and clears + any dirty blocks inside that range. + +[FIX] +The bug itself was introduced a long time ago, way before subpage and +large folio support. + +At that time, fs block size must match page size, thus the range +[cur, end) is just one fs block. + +But later with subpage and large folios, the same range [cur, end) +can have multiple blocks and ordered extents. + +Later commit 18de34daa7c6 ("btrfs: truncate ordered extent when skipping +writeback past i_size") was fixing a bug related to subpage/large +folios, but it's still utilizing the old range [cur, end), meaning only +the first OE will be marked as truncated. + +The proper fix here is to make EOF handling block-by-block, not trying +to handle the whole range to @end. + +By this we always locate and truncate the OE for every dirty block. + +CC: stable@vger.kernel.org # 5.15+ +Reviewed-by: Filipe Manana +Signed-off-by: Qu Wenruo +Signed-off-by: David Sterba +Signed-off-by: Sasha Levin +Signed-off-by: Greg Kroah-Hartman +--- + fs/btrfs/extent_io.c | 8 ++++---- + 1 file changed, 4 insertions(+), 4 deletions(-) + +--- a/fs/btrfs/extent_io.c ++++ b/fs/btrfs/extent_io.c +@@ -1730,7 +1730,7 @@ static noinline_for_stack int extent_wri + unsigned long flags; + + ordered = btrfs_lookup_first_ordered_range(inode, cur, +- folio_end - cur); ++ fs_info->sectorsize); + /* + * We have just run delalloc before getting here, so + * there must be an ordered extent. +@@ -1744,7 +1744,7 @@ static noinline_for_stack int extent_wri + btrfs_put_ordered_extent(ordered); + + btrfs_mark_ordered_io_finished(inode, folio, cur, +- end - cur, true); ++ fs_info->sectorsize, true); + /* + * This range is beyond i_size, thus we don't need to + * bother writing back. +@@ -1753,8 +1753,8 @@ static noinline_for_stack int extent_wri + * writeback the sectors with subpage dirty bits, + * causing writeback without ordered extent. + */ +- btrfs_folio_clear_dirty(fs_info, folio, cur, end - cur); +- break; ++ btrfs_folio_clear_dirty(fs_info, folio, cur, fs_info->sectorsize); ++ continue; + } + ret = submit_one_sector(inode, folio, cur, bio_ctrl, i_size); + if (unlikely(ret < 0)) { diff --git a/queue-6.18/btrfs-truncate-ordered-extent-when-skipping-writeback-past-i_size.patch b/queue-6.18/btrfs-truncate-ordered-extent-when-skipping-writeback-past-i_size.patch new file mode 100644 index 0000000000..2e6d063237 --- /dev/null +++ b/queue-6.18/btrfs-truncate-ordered-extent-when-skipping-writeback-past-i_size.patch @@ -0,0 +1,248 @@ +From stable+bounces-208118-greg=kroah.com@vger.kernel.org Mon Jan 12 14:54:57 2026 +From: Sasha Levin +Date: Mon, 12 Jan 2026 08:54:49 -0500 +Subject: btrfs: truncate ordered extent when skipping writeback past i_size +To: stable@vger.kernel.org +Cc: Filipe Manana , Qu Wenruo , Anand Jain , David Sterba , Sasha Levin +Message-ID: <20260112135451.704777-1-sashal@kernel.org> + +From: Filipe Manana + +[ Upstream commit 18de34daa7c62c830be533aace6b7c271e8e95cf ] + +While running test case btrfs/192 from fstests with support for large +folios (needs CONFIG_BTRFS_EXPERIMENTAL=y) I ended up getting very sporadic +btrfs check failures reporting that csum items were missing. Looking into +the issue it turned out that btrfs check searches for csum items of a file +extent item with a range that spans beyond the i_size of a file and we +don't have any, because the kernel's writeback code skips submitting bios +for ranges beyond eof. It's not expected however to find a file extent item +that crosses the rounded up (by the sector size) i_size value, but there is +a short time window where we can end up with a transaction commit leaving +this small inconsistency between the i_size and the last file extent item. + +Example btrfs check output when this happens: + + $ btrfs check /dev/sdc + Opening filesystem to check... + Checking filesystem on /dev/sdc + UUID: 69642c61-5efb-4367-aa31-cdfd4067f713 + [1/8] checking log skipped (none written) + [2/8] checking root items + [3/8] checking extents + [4/8] checking free space tree + [5/8] checking fs roots + root 5 inode 332 errors 1000, some csum missing + ERROR: errors found in fs roots + (...) + +Looking at a tree dump of the fs tree (root 5) for inode 332 we have: + + $ btrfs inspect-internal dump-tree -t 5 /dev/sdc + (...) + item 28 key (332 INODE_ITEM 0) itemoff 2006 itemsize 160 + generation 17 transid 19 size 610969 nbytes 86016 + block group 0 mode 100666 links 1 uid 0 gid 0 rdev 0 + sequence 11 flags 0x0(none) + atime 1759851068.391327881 (2025-10-07 16:31:08) + ctime 1759851068.410098267 (2025-10-07 16:31:08) + mtime 1759851068.410098267 (2025-10-07 16:31:08) + otime 1759851068.391327881 (2025-10-07 16:31:08) + item 29 key (332 INODE_REF 340) itemoff 1993 itemsize 13 + index 2 namelen 3 name: f1f + item 30 key (332 EXTENT_DATA 589824) itemoff 1940 itemsize 53 + generation 19 type 1 (regular) + extent data disk byte 21745664 nr 65536 + extent data offset 0 nr 65536 ram 65536 + extent compression 0 (none) + (...) + +We can see that the file extent item for file offset 589824 has a length of +64K and its number of bytes is 64K. Looking at the inode item we see that +its i_size is 610969 bytes which falls within the range of that file extent +item [589824, 655360[. + +Looking into the csum tree: + + $ btrfs inspect-internal dump-tree /dev/sdc + (...) + item 15 key (EXTENT_CSUM EXTENT_CSUM 21565440) itemoff 991 itemsize 200 + range start 21565440 end 21770240 length 204800 + item 16 key (EXTENT_CSUM EXTENT_CSUM 1104576512) itemoff 983 itemsize 8 + range start 1104576512 end 1104584704 length 8192 + (..) + +We see that the csum item number 15 covers the first 24K of the file extent +item - it ends at offset 21770240 and the extent's disk_bytenr is 21745664, +so we have: + + 21770240 - 21745664 = 24K + +We see that the next csum item (number 16) is completely outside the range, +so the remaining 40K of the extent doesn't have csum items in the tree. + +If we round up the i_size to the sector size, we get: + + round_up(610969, 4096) = 614400 + +If we subtract from that the file offset for the extent item we get: + + 614400 - 589824 = 24K + +So the missing 40K corresponds to the end of the file extent item's range +minus the rounded up i_size: + + 655360 - 614400 = 40K + +Normally we don't expect a file extent item to span over the rounded up +i_size of an inode, since when truncating, doing hole punching and other +operations that trim a file extent item, the number of bytes is adjusted. + +There is however a short time window where the kernel can end up, +temporarily,persisting an inode with an i_size that falls in the middle of +the last file extent item and the file extent item was not yet trimmed (its +number of bytes reduced so that it doesn't cross i_size rounded up by the +sector size). + +The steps (in the kernel) that lead to such scenario are the following: + + 1) We have inode I as an empty file, no allocated extents, i_size is 0; + + 2) A buffered write is done for file range [589824, 655360[ (length of + 64K) and the i_size is updated to 655360. Note that we got a single + large folio for the range (64K); + + 3) A truncate operation starts that reduces the inode's i_size down to + 610969 bytes. The truncate sets the inode's new i_size at + btrfs_setsize() by calling truncate_setsize() and before calling + btrfs_truncate(); + + 4) At btrfs_truncate() we trigger writeback for the range starting at + 610304 (which is the new i_size rounded down to the sector size) and + ending at (u64)-1; + + 5) During the writeback, at extent_write_cache_pages(), we get from the + call to filemap_get_folios_tag(), the 64K folio that starts at file + offset 589824 since it contains the start offset of the writeback + range (610304); + + 6) At writepage_delalloc() we find the whole range of the folio is dirty + and therefore we run delalloc for that 64K range ([589824, 655360[), + reserving a 64K extent, creating an ordered extent, etc; + + 7) At extent_writepage_io() we submit IO only for subrange [589824, 614400[ + because the inode's i_size is 610969 bytes (rounded up by sector size + is 614400). There, in the while loop we intentionally skip IO beyond + i_size to avoid any unnecessay work and just call + btrfs_mark_ordered_io_finished() for the range [614400, 655360[ (which + has a 40K length); + + 8) Once the IO finishes we finish the ordered extent by ending up at + btrfs_finish_one_ordered(), join transaction N, insert a file extent + item in the inode's subvolume tree for file offset 589824 with a number + of bytes of 64K, and update the inode's delayed inode item or directly + the inode item with a call to btrfs_update_inode_fallback(), which + results in storing the new i_size of 610969 bytes; + + 9) Transaction N is committed either by the transaction kthread or some + other task committed it (in response to a sync or fsync for example). + + At this point we have inode I persisted with an i_size of 610969 bytes + and file extent item that starts at file offset 589824 and has a number + of bytes of 64K, ending at an offset of 655360 which is beyond the + i_size rounded up to the sector size (614400). + + --> So after a crash or power failure here, the btrfs check program + reports that error about missing checksum items for this inode, as + it tries to lookup for checksums covering the whole range of the + extent; + +10) Only after transaction N is committed that at btrfs_truncate() the + call to btrfs_start_transaction() starts a new transaction, N + 1, + instead of joining transaction N. And it's with transaction N + 1 that + it calls btrfs_truncate_inode_items() which updates the file extent + item at file offset 589824 to reduce its number of bytes from 64K down + to 24K, so that the file extent item's range ends at the i_size + rounded up to the sector size (614400 bytes). + +Fix this by truncating the ordered extent at extent_writepage_io() when we +skip writeback because the current offset in the folio is beyond i_size. +This ensures we don't ever persist a file extent item with a number of +bytes beyond the rounded up (by sector size) value of the i_size. + +Reviewed-by: Qu Wenruo +Reviewed-by: Anand Jain +Signed-off-by: Filipe Manana +Signed-off-by: David Sterba +Stable-dep-of: e9e3b22ddfa7 ("btrfs: fix beyond-EOF write handling") +Signed-off-by: Sasha Levin +Signed-off-by: Greg Kroah-Hartman +--- + fs/btrfs/extent_io.c | 21 +++++++++++++++++++-- + fs/btrfs/ordered-data.c | 5 +++-- + 2 files changed, 22 insertions(+), 4 deletions(-) + +--- a/fs/btrfs/extent_io.c ++++ b/fs/btrfs/extent_io.c +@@ -1692,13 +1692,13 @@ static noinline_for_stack int extent_wri + bool submitted_io = false; + int found_error = 0; + const u64 folio_start = folio_pos(folio); ++ const u64 folio_end = folio_start + folio_size(folio); + const unsigned int blocks_per_folio = btrfs_blocks_per_folio(fs_info, folio); + u64 cur; + int bit; + int ret = 0; + +- ASSERT(start >= folio_start && +- start + len <= folio_start + folio_size(folio)); ++ ASSERT(start >= folio_start && start + len <= folio_end); + + ret = btrfs_writepage_cow_fixup(folio); + if (ret == -EAGAIN) { +@@ -1725,6 +1725,23 @@ static noinline_for_stack int extent_wri + cur = folio_pos(folio) + (bit << fs_info->sectorsize_bits); + + if (cur >= i_size) { ++ struct btrfs_ordered_extent *ordered; ++ unsigned long flags; ++ ++ ordered = btrfs_lookup_first_ordered_range(inode, cur, ++ folio_end - cur); ++ /* ++ * We have just run delalloc before getting here, so ++ * there must be an ordered extent. ++ */ ++ ASSERT(ordered != NULL); ++ spin_lock_irqsave(&inode->ordered_tree_lock, flags); ++ set_bit(BTRFS_ORDERED_TRUNCATED, &ordered->flags); ++ ordered->truncated_len = min(ordered->truncated_len, ++ cur - ordered->file_offset); ++ spin_unlock_irqrestore(&inode->ordered_tree_lock, flags); ++ btrfs_put_ordered_extent(ordered); ++ + btrfs_mark_ordered_io_finished(inode, folio, cur, + start + len - cur, true); + /* +--- a/fs/btrfs/ordered-data.c ++++ b/fs/btrfs/ordered-data.c +@@ -1098,8 +1098,9 @@ struct btrfs_ordered_extent *btrfs_looku + struct rb_node *prev; + struct rb_node *next; + struct btrfs_ordered_extent *entry = NULL; ++ unsigned long flags; + +- spin_lock_irq(&inode->ordered_tree_lock); ++ spin_lock_irqsave(&inode->ordered_tree_lock, flags); + node = inode->ordered_tree.rb_node; + /* + * Here we don't want to use tree_search() which will use tree->last +@@ -1154,7 +1155,7 @@ out: + trace_btrfs_ordered_extent_lookup_first_range(inode, entry); + } + +- spin_unlock_irq(&inode->ordered_tree_lock); ++ spin_unlock_irqrestore(&inode->ordered_tree_lock, flags); + return entry; + } + diff --git a/queue-6.18/btrfs-use-variable-for-end-offset-in-extent_writepage_io.patch b/queue-6.18/btrfs-use-variable-for-end-offset-in-extent_writepage_io.patch new file mode 100644 index 0000000000..f73c4ef502 --- /dev/null +++ b/queue-6.18/btrfs-use-variable-for-end-offset-in-extent_writepage_io.patch @@ -0,0 +1,74 @@ +From stable+bounces-208119-greg=kroah.com@vger.kernel.org Mon Jan 12 14:56:54 2026 +From: Sasha Levin +Date: Mon, 12 Jan 2026 08:54:50 -0500 +Subject: btrfs: use variable for end offset in extent_writepage_io() +To: stable@vger.kernel.org +Cc: Filipe Manana , Qu Wenruo , Anand Jain , David Sterba , Sasha Levin +Message-ID: <20260112135451.704777-2-sashal@kernel.org> + +From: Filipe Manana + +[ Upstream commit 46a23908598f4b8e61483f04ea9f471b2affc58a ] + +Instead of repeating the expression "start + len" multiple times, store it +in a variable and use it where needed. + +Reviewed-by: Qu Wenruo +Reviewed-by: Anand Jain +Signed-off-by: Filipe Manana +Reviewed-by: David Sterba +Signed-off-by: David Sterba +Stable-dep-of: e9e3b22ddfa7 ("btrfs: fix beyond-EOF write handling") +Signed-off-by: Sasha Levin +Signed-off-by: Greg Kroah-Hartman +--- + fs/btrfs/extent_io.c | 10 +++++----- + 1 file changed, 5 insertions(+), 5 deletions(-) + +--- a/fs/btrfs/extent_io.c ++++ b/fs/btrfs/extent_io.c +@@ -1691,6 +1691,7 @@ static noinline_for_stack int extent_wri + unsigned long range_bitmap = 0; + bool submitted_io = false; + int found_error = 0; ++ const u64 end = start + len; + const u64 folio_start = folio_pos(folio); + const u64 folio_end = folio_start + folio_size(folio); + const unsigned int blocks_per_folio = btrfs_blocks_per_folio(fs_info, folio); +@@ -1698,7 +1699,7 @@ static noinline_for_stack int extent_wri + int bit; + int ret = 0; + +- ASSERT(start >= folio_start && start + len <= folio_end); ++ ASSERT(start >= folio_start && end <= folio_end); + + ret = btrfs_writepage_cow_fixup(folio); + if (ret == -EAGAIN) { +@@ -1714,7 +1715,7 @@ static noinline_for_stack int extent_wri + return ret; + } + +- for (cur = start; cur < start + len; cur += fs_info->sectorsize) ++ for (cur = start; cur < end; cur += fs_info->sectorsize) + set_bit((cur - folio_start) >> fs_info->sectorsize_bits, &range_bitmap); + bitmap_and(&bio_ctrl->submit_bitmap, &bio_ctrl->submit_bitmap, &range_bitmap, + blocks_per_folio); +@@ -1743,7 +1744,7 @@ static noinline_for_stack int extent_wri + btrfs_put_ordered_extent(ordered); + + btrfs_mark_ordered_io_finished(inode, folio, cur, +- start + len - cur, true); ++ end - cur, true); + /* + * This range is beyond i_size, thus we don't need to + * bother writing back. +@@ -1752,8 +1753,7 @@ static noinline_for_stack int extent_wri + * writeback the sectors with subpage dirty bits, + * causing writeback without ordered extent. + */ +- btrfs_folio_clear_dirty(fs_info, folio, cur, +- start + len - cur); ++ btrfs_folio_clear_dirty(fs_info, folio, cur, end - cur); + break; + } + ret = submit_one_sector(inode, folio, cur, bio_ctrl, i_size); diff --git a/queue-6.18/gpio-mpsse-add-quirk-support.patch b/queue-6.18/gpio-mpsse-add-quirk-support.patch new file mode 100644 index 0000000000..22e56ac575 --- /dev/null +++ b/queue-6.18/gpio-mpsse-add-quirk-support.patch @@ -0,0 +1,246 @@ +From stable+bounces-208190-greg=kroah.com@vger.kernel.org Mon Jan 12 18:45:11 2026 +From: Sasha Levin +Date: Mon, 12 Jan 2026 12:44:40 -0500 +Subject: gpio: mpsse: add quirk support +To: stable@vger.kernel.org +Cc: Mary Strodl , Dan Carpenter , Linus Walleij , Bartosz Golaszewski , Sasha Levin +Message-ID: <20260112174441.830780-2-sashal@kernel.org> + +From: Mary Strodl + +[ Upstream commit f13b0f72af238d63bb9a2e417657da8b45d72544 ] + +Builds out a facility for specifying compatible lines directions and +labels for MPSSE-based devices. + +* dir_in/out are bitmask of lines that can go in/out. 1 means + compatible, 0 means incompatible. +* names is an array of line names which will be exposed to userspace. + +Also changes the chip label format to include some more useful +information about the device to help identify it from userspace. + +Signed-off-by: Mary Strodl +Reviewed-by: Dan Carpenter +Reviewed-by: Linus Walleij +Link: https://lore.kernel.org/r/20251014133530.3592716-4-mstrodl@csh.rit.edu +Signed-off-by: Bartosz Golaszewski +Stable-dep-of: 1e876e5a0875 ("gpio: mpsse: fix reference leak in gpio_mpsse_probe() error paths") +Signed-off-by: Sasha Levin +Signed-off-by: Greg Kroah-Hartman +--- + drivers/gpio/gpio-mpsse.c | 109 ++++++++++++++++++++++++++++++++++++++++++++-- + 1 file changed, 106 insertions(+), 3 deletions(-) + +--- a/drivers/gpio/gpio-mpsse.c ++++ b/drivers/gpio/gpio-mpsse.c +@@ -29,6 +29,9 @@ struct mpsse_priv { + u8 gpio_outputs[2]; /* Output states for GPIOs [L, H] */ + u8 gpio_dir[2]; /* Directions for GPIOs [L, H] */ + ++ unsigned long dir_in; /* Bitmask of valid input pins */ ++ unsigned long dir_out; /* Bitmask of valid output pins */ ++ + u8 *bulk_in_buf; /* Extra recv buffer to grab status bytes */ + + struct usb_endpoint_descriptor *bulk_in; +@@ -54,6 +57,14 @@ struct bulk_desc { + int timeout; + }; + ++#define MPSSE_NGPIO 16 ++ ++struct mpsse_quirk { ++ const char *names[MPSSE_NGPIO]; /* Pin names, if applicable */ ++ unsigned long dir_in; /* Bitmask of valid input pins */ ++ unsigned long dir_out; /* Bitmask of valid output pins */ ++}; ++ + static const struct usb_device_id gpio_mpsse_table[] = { + { USB_DEVICE(0x0c52, 0xa064) }, /* SeaLevel Systems, Inc. */ + { } /* Terminating entry */ +@@ -171,6 +182,32 @@ static int gpio_mpsse_get_bank(struct mp + return buf; + } + ++static int mpsse_ensure_supported(struct gpio_chip *chip, ++ unsigned long mask, int direction) ++{ ++ unsigned long supported, unsupported; ++ char *type = "input"; ++ struct mpsse_priv *priv = gpiochip_get_data(chip); ++ ++ supported = priv->dir_in; ++ if (direction == GPIO_LINE_DIRECTION_OUT) { ++ supported = priv->dir_out; ++ type = "output"; ++ } ++ ++ /* An invalid bit was in the provided mask */ ++ unsupported = mask & ~supported; ++ if (unsupported) { ++ dev_err(&priv->udev->dev, ++ "mpsse: GPIO %lu doesn't support %s\n", ++ find_first_bit(&unsupported, sizeof(unsupported) * 8), ++ type); ++ return -EOPNOTSUPP; ++ } ++ ++ return 0; ++} ++ + static int gpio_mpsse_set_multiple(struct gpio_chip *chip, unsigned long *mask, + unsigned long *bits) + { +@@ -178,6 +215,10 @@ static int gpio_mpsse_set_multiple(struc + int ret; + struct mpsse_priv *priv = gpiochip_get_data(chip); + ++ ret = mpsse_ensure_supported(chip, *mask, GPIO_LINE_DIRECTION_OUT); ++ if (ret) ++ return ret; ++ + guard(mutex)(&priv->io_mutex); + for_each_set_clump8(i, bank_mask, mask, chip->ngpio) { + bank = i / 8; +@@ -205,6 +246,10 @@ static int gpio_mpsse_get_multiple(struc + int ret; + struct mpsse_priv *priv = gpiochip_get_data(chip); + ++ ret = mpsse_ensure_supported(chip, *mask, GPIO_LINE_DIRECTION_IN); ++ if (ret) ++ return ret; ++ + guard(mutex)(&priv->io_mutex); + for_each_set_clump8(i, bank_mask, mask, chip->ngpio) { + bank = i / 8; +@@ -253,10 +298,15 @@ static int gpio_mpsse_gpio_set(struct gp + static int gpio_mpsse_direction_output(struct gpio_chip *chip, + unsigned int offset, int value) + { ++ int ret; + struct mpsse_priv *priv = gpiochip_get_data(chip); + int bank = (offset & 8) >> 3; + int bank_offset = offset & 7; + ++ ret = mpsse_ensure_supported(chip, BIT(offset), GPIO_LINE_DIRECTION_OUT); ++ if (ret) ++ return ret; ++ + scoped_guard(mutex, &priv->io_mutex) + priv->gpio_dir[bank] |= BIT(bank_offset); + +@@ -266,10 +316,15 @@ static int gpio_mpsse_direction_output(s + static int gpio_mpsse_direction_input(struct gpio_chip *chip, + unsigned int offset) + { ++ int ret; + struct mpsse_priv *priv = gpiochip_get_data(chip); + int bank = (offset & 8) >> 3; + int bank_offset = offset & 7; + ++ ret = mpsse_ensure_supported(chip, BIT(offset), GPIO_LINE_DIRECTION_IN); ++ if (ret) ++ return ret; ++ + guard(mutex)(&priv->io_mutex); + priv->gpio_dir[bank] &= ~BIT(bank_offset); + gpio_mpsse_set_bank(priv, bank); +@@ -483,18 +538,49 @@ static void gpio_mpsse_ida_remove(void * + ida_free(&gpio_mpsse_ida, priv->id); + } + ++static int mpsse_init_valid_mask(struct gpio_chip *chip, ++ unsigned long *valid_mask, ++ unsigned int ngpios) ++{ ++ struct mpsse_priv *priv = gpiochip_get_data(chip); ++ ++ if (WARN_ON(priv == NULL)) ++ return -ENODEV; ++ ++ *valid_mask = priv->dir_in | priv->dir_out; ++ ++ return 0; ++} ++ ++static void mpsse_irq_init_valid_mask(struct gpio_chip *chip, ++ unsigned long *valid_mask, ++ unsigned int ngpios) ++{ ++ struct mpsse_priv *priv = gpiochip_get_data(chip); ++ ++ if (WARN_ON(priv == NULL)) ++ return; ++ ++ /* Can only use IRQ on input capable pins */ ++ *valid_mask = priv->dir_in; ++} ++ + static int gpio_mpsse_probe(struct usb_interface *interface, + const struct usb_device_id *id) + { + struct mpsse_priv *priv; + struct device *dev; ++ char *serial; + int err; ++ struct mpsse_quirk *quirk = (void *)id->driver_info; + + dev = &interface->dev; + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + ++ INIT_LIST_HEAD(&priv->workers); ++ + priv->udev = usb_get_dev(interface_to_usbdev(interface)); + priv->intf = interface; + priv->intf_id = interface->cur_altsetting->desc.bInterfaceNumber; +@@ -521,9 +607,15 @@ static int gpio_mpsse_probe(struct usb_i + + raw_spin_lock_init(&priv->irq_spin); + ++ serial = priv->udev->serial; ++ if (!serial) ++ serial = "NONE"; ++ + priv->gpio.label = devm_kasprintf(dev, GFP_KERNEL, +- "gpio-mpsse.%d.%d", +- priv->id, priv->intf_id); ++ "MPSSE%04x:%04x.%d.%d.%s", ++ id->idVendor, id->idProduct, ++ priv->intf_id, priv->id, ++ serial); + if (!priv->gpio.label) + return -ENOMEM; + +@@ -537,10 +629,20 @@ static int gpio_mpsse_probe(struct usb_i + priv->gpio.get_multiple = gpio_mpsse_get_multiple; + priv->gpio.set_multiple = gpio_mpsse_set_multiple; + priv->gpio.base = -1; +- priv->gpio.ngpio = 16; ++ priv->gpio.ngpio = MPSSE_NGPIO; + priv->gpio.offset = priv->intf_id * priv->gpio.ngpio; + priv->gpio.can_sleep = 1; + ++ if (quirk) { ++ priv->dir_out = quirk->dir_out; ++ priv->dir_in = quirk->dir_in; ++ priv->gpio.names = quirk->names; ++ priv->gpio.init_valid_mask = mpsse_init_valid_mask; ++ } else { ++ priv->dir_in = U16_MAX; ++ priv->dir_out = U16_MAX; ++ } ++ + err = usb_find_common_endpoints(interface->cur_altsetting, + &priv->bulk_in, &priv->bulk_out, + NULL, NULL); +@@ -579,6 +681,7 @@ static int gpio_mpsse_probe(struct usb_i + priv->gpio.irq.parents = NULL; + priv->gpio.irq.default_type = IRQ_TYPE_NONE; + priv->gpio.irq.handler = handle_simple_irq; ++ priv->gpio.irq.init_valid_mask = mpsse_irq_init_valid_mask; + + err = devm_gpiochip_add_data(dev, &priv->gpio, priv); + if (err) diff --git a/queue-6.18/gpio-mpsse-ensure-worker-is-torn-down.patch b/queue-6.18/gpio-mpsse-ensure-worker-is-torn-down.patch new file mode 100644 index 0000000000..c48ff99ab4 --- /dev/null +++ b/queue-6.18/gpio-mpsse-ensure-worker-is-torn-down.patch @@ -0,0 +1,209 @@ +From stable+bounces-208189-greg=kroah.com@vger.kernel.org Mon Jan 12 18:48:50 2026 +From: Sasha Levin +Date: Mon, 12 Jan 2026 12:44:39 -0500 +Subject: gpio: mpsse: ensure worker is torn down +To: stable@vger.kernel.org +Cc: Mary Strodl , Linus Walleij , Bartosz Golaszewski , Sasha Levin +Message-ID: <20260112174441.830780-1-sashal@kernel.org> + +From: Mary Strodl + +[ Upstream commit 179ef1127d7a4f09f0e741fa9f30b8a8e7886271 ] + +When an IRQ worker is running, unplugging the device would cause a +crash. The sealevel hardware this driver was written for was not +hotpluggable, so I never realized it. + +This change uses a spinlock to protect a list of workers, which +it tears down on disconnect. + +Signed-off-by: Mary Strodl +Reviewed-by: Linus Walleij +Link: https://lore.kernel.org/r/20251014133530.3592716-3-mstrodl@csh.rit.edu +Signed-off-by: Bartosz Golaszewski +Stable-dep-of: 1e876e5a0875 ("gpio: mpsse: fix reference leak in gpio_mpsse_probe() error paths") +Signed-off-by: Sasha Levin +Signed-off-by: Greg Kroah-Hartman +--- + drivers/gpio/gpio-mpsse.c | 106 ++++++++++++++++++++++++++++++++++++++++++---- + 1 file changed, 99 insertions(+), 7 deletions(-) + +--- a/drivers/gpio/gpio-mpsse.c ++++ b/drivers/gpio/gpio-mpsse.c +@@ -10,6 +10,7 @@ + #include + #include + #include ++#include + #include + + struct mpsse_priv { +@@ -17,8 +18,10 @@ struct mpsse_priv { + struct usb_device *udev; /* USB device encompassing all MPSSEs */ + struct usb_interface *intf; /* USB interface for this MPSSE */ + u8 intf_id; /* USB interface number for this MPSSE */ +- struct work_struct irq_work; /* polling work thread */ ++ struct list_head workers; /* polling work threads */ + struct mutex irq_mutex; /* lock over irq_data */ ++ struct mutex irq_race; /* race for polling worker teardown */ ++ raw_spinlock_t irq_spin; /* protects worker list */ + atomic_t irq_type[16]; /* pin -> edge detection type */ + atomic_t irq_enabled; + int id; +@@ -34,6 +37,14 @@ struct mpsse_priv { + struct mutex io_mutex; /* sync I/O with disconnect */ + }; + ++struct mpsse_worker { ++ struct mpsse_priv *priv; ++ struct work_struct work; ++ atomic_t cancelled; ++ struct list_head list; /* linked list */ ++ struct list_head destroy; /* teardown linked list */ ++}; ++ + struct bulk_desc { + bool tx; /* direction of bulk transfer */ + u8 *data; /* input (tx) or output (rx) */ +@@ -284,18 +295,62 @@ static int gpio_mpsse_get_direction(stru + return ret; + } + +-static void gpio_mpsse_poll(struct work_struct *work) ++/* ++ * Stops all workers except `my_worker`. ++ * Safe to call only when `irq_race` is held. ++ */ ++static void gpio_mpsse_stop_all_except(struct mpsse_priv *priv, ++ struct mpsse_worker *my_worker) ++{ ++ struct mpsse_worker *worker, *worker_tmp; ++ struct list_head destructors = LIST_HEAD_INIT(destructors); ++ ++ scoped_guard(raw_spinlock_irqsave, &priv->irq_spin) { ++ list_for_each_entry_safe(worker, worker_tmp, ++ &priv->workers, list) { ++ /* Don't stop ourselves */ ++ if (worker == my_worker) ++ continue; ++ ++ list_del(&worker->list); ++ ++ /* Give worker a chance to terminate itself */ ++ atomic_set(&worker->cancelled, 1); ++ /* Keep track of stuff to cancel */ ++ INIT_LIST_HEAD(&worker->destroy); ++ list_add(&worker->destroy, &destructors); ++ } ++ } ++ ++ list_for_each_entry_safe(worker, worker_tmp, ++ &destructors, destroy) { ++ list_del(&worker->destroy); ++ cancel_work_sync(&worker->work); ++ kfree(worker); ++ } ++} ++ ++static void gpio_mpsse_poll(struct work_struct *my_work) + { + unsigned long pin_mask, pin_states, flags; + int irq_enabled, offset, err, value, fire_irq, + irq, old_value[16], irq_type[16]; +- struct mpsse_priv *priv = container_of(work, struct mpsse_priv, +- irq_work); ++ struct mpsse_worker *my_worker = container_of(my_work, struct mpsse_worker, work); ++ struct mpsse_priv *priv = my_worker->priv; + + for (offset = 0; offset < priv->gpio.ngpio; ++offset) + old_value[offset] = -1; + +- while ((irq_enabled = atomic_read(&priv->irq_enabled))) { ++ /* ++ * We only want one worker. Workers race to acquire irq_race and tear ++ * down all other workers. This is a cond guard so that we don't deadlock ++ * trying to cancel a worker. ++ */ ++ scoped_cond_guard(mutex_try, return, &priv->irq_race) ++ gpio_mpsse_stop_all_except(priv, my_worker); ++ ++ while ((irq_enabled = atomic_read(&priv->irq_enabled)) && ++ !atomic_read(&my_worker->cancelled)) { + usleep_range(MPSSE_POLL_INTERVAL, MPSSE_POLL_INTERVAL + 1000); + /* Cleanup will trigger at the end of the loop */ + guard(mutex)(&priv->irq_mutex); +@@ -370,21 +425,45 @@ static int gpio_mpsse_set_irq_type(struc + + static void gpio_mpsse_irq_disable(struct irq_data *irqd) + { ++ struct mpsse_worker *worker; + struct mpsse_priv *priv = irq_data_get_irq_chip_data(irqd); + + atomic_and(~BIT(irqd->hwirq), &priv->irq_enabled); + gpiochip_disable_irq(&priv->gpio, irqd->hwirq); ++ ++ /* ++ * Can't actually do teardown in IRQ context (it blocks). ++ * As a result, these workers will stick around until irq is reenabled ++ * or device gets disconnected ++ */ ++ scoped_guard(raw_spinlock_irqsave, &priv->irq_spin) ++ list_for_each_entry(worker, &priv->workers, list) ++ atomic_set(&worker->cancelled, 1); + } + + static void gpio_mpsse_irq_enable(struct irq_data *irqd) + { ++ struct mpsse_worker *worker; + struct mpsse_priv *priv = irq_data_get_irq_chip_data(irqd); + + gpiochip_enable_irq(&priv->gpio, irqd->hwirq); + /* If no-one else was using the IRQ, enable it */ + if (!atomic_fetch_or(BIT(irqd->hwirq), &priv->irq_enabled)) { +- INIT_WORK(&priv->irq_work, gpio_mpsse_poll); +- schedule_work(&priv->irq_work); ++ /* ++ * Can't be devm because it uses a non-raw spinlock (illegal in ++ * this context, where a raw spinlock is held by our caller) ++ */ ++ worker = kzalloc(sizeof(*worker), GFP_NOWAIT); ++ if (!worker) ++ return; ++ ++ worker->priv = priv; ++ INIT_LIST_HEAD(&worker->list); ++ INIT_WORK(&worker->work, gpio_mpsse_poll); ++ schedule_work(&worker->work); ++ ++ scoped_guard(raw_spinlock_irqsave, &priv->irq_spin) ++ list_add(&worker->list, &priv->workers); + } + } + +@@ -436,6 +515,12 @@ static int gpio_mpsse_probe(struct usb_i + if (err) + return err; + ++ err = devm_mutex_init(dev, &priv->irq_race); ++ if (err) ++ return err; ++ ++ raw_spin_lock_init(&priv->irq_spin); ++ + priv->gpio.label = devm_kasprintf(dev, GFP_KERNEL, + "gpio-mpsse.%d.%d", + priv->id, priv->intf_id); +@@ -506,6 +591,13 @@ static void gpio_mpsse_disconnect(struct + { + struct mpsse_priv *priv = usb_get_intfdata(intf); + ++ /* ++ * Lock prevents double-free of worker from here and the teardown ++ * step at the beginning of gpio_mpsse_poll ++ */ ++ scoped_guard(mutex, &priv->irq_race) ++ gpio_mpsse_stop_all_except(priv, NULL); ++ + priv->intf = NULL; + usb_set_intfdata(intf, NULL); + usb_put_dev(priv->udev); diff --git a/queue-6.18/gpio-mpsse-fix-reference-leak-in-gpio_mpsse_probe-error-paths.patch b/queue-6.18/gpio-mpsse-fix-reference-leak-in-gpio_mpsse_probe-error-paths.patch new file mode 100644 index 0000000000..77d1e42a2b --- /dev/null +++ b/queue-6.18/gpio-mpsse-fix-reference-leak-in-gpio_mpsse_probe-error-paths.patch @@ -0,0 +1,63 @@ +From stable+bounces-208191-greg=kroah.com@vger.kernel.org Mon Jan 12 18:45:15 2026 +From: Sasha Levin +Date: Mon, 12 Jan 2026 12:44:41 -0500 +Subject: gpio: mpsse: fix reference leak in gpio_mpsse_probe() error paths +To: stable@vger.kernel.org +Cc: Abdun Nihaal , Bartosz Golaszewski , Sasha Levin +Message-ID: <20260112174441.830780-3-sashal@kernel.org> + +From: Abdun Nihaal + +[ Upstream commit 1e876e5a0875e71e34148c9feb2eedd3bf6b2b43 ] + +The reference obtained by calling usb_get_dev() is not released in the +gpio_mpsse_probe() error paths. Fix that by using device managed helper +functions. Also remove the usb_put_dev() call in the disconnect function +since now it will be released automatically. + +Cc: stable@vger.kernel.org +Fixes: c46a74ff05c0 ("gpio: add support for FTDI's MPSSE as GPIO") +Signed-off-by: Abdun Nihaal +Link: https://lore.kernel.org/r/20251226060414.20785-1-nihaal@cse.iitm.ac.in +Signed-off-by: Bartosz Golaszewski +Signed-off-by: Sasha Levin +Signed-off-by: Greg Kroah-Hartman +--- + drivers/gpio/gpio-mpsse.c | 12 +++++++++++- + 1 file changed, 11 insertions(+), 1 deletion(-) + +--- a/drivers/gpio/gpio-mpsse.c ++++ b/drivers/gpio/gpio-mpsse.c +@@ -538,6 +538,13 @@ static void gpio_mpsse_ida_remove(void * + ida_free(&gpio_mpsse_ida, priv->id); + } + ++static void gpio_mpsse_usb_put_dev(void *data) ++{ ++ struct mpsse_priv *priv = data; ++ ++ usb_put_dev(priv->udev); ++} ++ + static int mpsse_init_valid_mask(struct gpio_chip *chip, + unsigned long *valid_mask, + unsigned int ngpios) +@@ -582,6 +589,10 @@ static int gpio_mpsse_probe(struct usb_i + INIT_LIST_HEAD(&priv->workers); + + priv->udev = usb_get_dev(interface_to_usbdev(interface)); ++ err = devm_add_action_or_reset(dev, gpio_mpsse_usb_put_dev, priv); ++ if (err) ++ return err; ++ + priv->intf = interface; + priv->intf_id = interface->cur_altsetting->desc.bInterfaceNumber; + +@@ -703,7 +714,6 @@ static void gpio_mpsse_disconnect(struct + + priv->intf = NULL; + usb_set_intfdata(intf, NULL); +- usb_put_dev(priv->udev); + } + + static struct usb_driver gpio_mpsse_driver = { diff --git a/queue-6.18/series b/queue-6.18/series index c8b4bc4e67..252f790d3d 100644 --- a/queue-6.18/series +++ b/queue-6.18/series @@ -155,3 +155,9 @@ ublk-fix-use-after-free-in-ublk_partition_scan_work.patch irqchip-gic-v5-fix-gicv5_its_map_event-itte-read-end.patch erofs-don-t-bother-with-s_stack_depth-increasing-for.patch erofs-fix-file-backed-mounts-no-longer-working-on-er.patch +btrfs-truncate-ordered-extent-when-skipping-writeback-past-i_size.patch +btrfs-use-variable-for-end-offset-in-extent_writepage_io.patch +btrfs-fix-beyond-eof-write-handling.patch +gpio-mpsse-ensure-worker-is-torn-down.patch +gpio-mpsse-add-quirk-support.patch +gpio-mpsse-fix-reference-leak-in-gpio_mpsse_probe-error-paths.patch