--- /dev/null
+From stable+bounces-208120-greg=kroah.com@vger.kernel.org Mon Jan 12 14:55:01 2026
+From: Sasha Levin <sashal@kernel.org>
+Date: Mon, 12 Jan 2026 08:54:51 -0500
+Subject: btrfs: fix beyond-EOF write handling
+To: stable@vger.kernel.org
+Cc: Qu Wenruo <wqu@suse.com>, Filipe Manana <fdmanana@suse.com>, David Sterba <dsterba@suse.com>, Sasha Levin <sashal@kernel.org>
+Message-ID: <20260112135451.704777-3-sashal@kernel.org>
+
+From: Qu Wenruo <wqu@suse.com>
+
+[ 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 <fdmanana@suse.com>
+Signed-off-by: Qu Wenruo <wqu@suse.com>
+Signed-off-by: David Sterba <dsterba@suse.com>
+Signed-off-by: Sasha Levin <sashal@kernel.org>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ 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)) {
--- /dev/null
+From stable+bounces-208118-greg=kroah.com@vger.kernel.org Mon Jan 12 14:54:57 2026
+From: Sasha Levin <sashal@kernel.org>
+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 <fdmanana@suse.com>, Qu Wenruo <wqu@suse.com>, Anand Jain <asj@kernel.org>, David Sterba <dsterba@suse.com>, Sasha Levin <sashal@kernel.org>
+Message-ID: <20260112135451.704777-1-sashal@kernel.org>
+
+From: Filipe Manana <fdmanana@suse.com>
+
+[ 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 <wqu@suse.com>
+Reviewed-by: Anand Jain <asj@kernel.org>
+Signed-off-by: Filipe Manana <fdmanana@suse.com>
+Signed-off-by: David Sterba <dsterba@suse.com>
+Stable-dep-of: e9e3b22ddfa7 ("btrfs: fix beyond-EOF write handling")
+Signed-off-by: Sasha Levin <sashal@kernel.org>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ 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;
+ }
+
--- /dev/null
+From stable+bounces-208119-greg=kroah.com@vger.kernel.org Mon Jan 12 14:56:54 2026
+From: Sasha Levin <sashal@kernel.org>
+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 <fdmanana@suse.com>, Qu Wenruo <wqu@suse.com>, Anand Jain <asj@kernel.org>, David Sterba <dsterba@suse.com>, Sasha Levin <sashal@kernel.org>
+Message-ID: <20260112135451.704777-2-sashal@kernel.org>
+
+From: Filipe Manana <fdmanana@suse.com>
+
+[ 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 <wqu@suse.com>
+Reviewed-by: Anand Jain <asj@kernel.org>
+Signed-off-by: Filipe Manana <fdmanana@suse.com>
+Reviewed-by: David Sterba <dsterba@suse.com>
+Signed-off-by: David Sterba <dsterba@suse.com>
+Stable-dep-of: e9e3b22ddfa7 ("btrfs: fix beyond-EOF write handling")
+Signed-off-by: Sasha Levin <sashal@kernel.org>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ 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);
--- /dev/null
+From stable+bounces-208190-greg=kroah.com@vger.kernel.org Mon Jan 12 18:45:11 2026
+From: Sasha Levin <sashal@kernel.org>
+Date: Mon, 12 Jan 2026 12:44:40 -0500
+Subject: gpio: mpsse: add quirk support
+To: stable@vger.kernel.org
+Cc: Mary Strodl <mstrodl@csh.rit.edu>, Dan Carpenter <dan.carpenter@linaro.org>, Linus Walleij <linus.walleij@linaro.org>, Bartosz Golaszewski <bartosz.golaszewski@linaro.org>, Sasha Levin <sashal@kernel.org>
+Message-ID: <20260112174441.830780-2-sashal@kernel.org>
+
+From: Mary Strodl <mstrodl@csh.rit.edu>
+
+[ 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 <mstrodl@csh.rit.edu>
+Reviewed-by: Dan Carpenter <dan.carpenter@linaro.org>
+Reviewed-by: Linus Walleij <linus.walleij@linaro.org>
+Link: https://lore.kernel.org/r/20251014133530.3592716-4-mstrodl@csh.rit.edu
+Signed-off-by: Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+Stable-dep-of: 1e876e5a0875 ("gpio: mpsse: fix reference leak in gpio_mpsse_probe() error paths")
+Signed-off-by: Sasha Levin <sashal@kernel.org>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ 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)
--- /dev/null
+From stable+bounces-208189-greg=kroah.com@vger.kernel.org Mon Jan 12 18:48:50 2026
+From: Sasha Levin <sashal@kernel.org>
+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 <mstrodl@csh.rit.edu>, Linus Walleij <linus.walleij@linaro.org>, Bartosz Golaszewski <bartosz.golaszewski@linaro.org>, Sasha Levin <sashal@kernel.org>
+Message-ID: <20260112174441.830780-1-sashal@kernel.org>
+
+From: Mary Strodl <mstrodl@csh.rit.edu>
+
+[ 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 <mstrodl@csh.rit.edu>
+Reviewed-by: Linus Walleij <linus.walleij@linaro.org>
+Link: https://lore.kernel.org/r/20251014133530.3592716-3-mstrodl@csh.rit.edu
+Signed-off-by: Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
+Stable-dep-of: 1e876e5a0875 ("gpio: mpsse: fix reference leak in gpio_mpsse_probe() error paths")
+Signed-off-by: Sasha Levin <sashal@kernel.org>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ 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 <linux/cleanup.h>
+ #include <linux/gpio/driver.h>
+ #include <linux/mutex.h>
++#include <linux/spinlock.h>
+ #include <linux/usb.h>
+
+ 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);
--- /dev/null
+From stable+bounces-208191-greg=kroah.com@vger.kernel.org Mon Jan 12 18:45:15 2026
+From: Sasha Levin <sashal@kernel.org>
+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 <nihaal@cse.iitm.ac.in>, Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>, Sasha Levin <sashal@kernel.org>
+Message-ID: <20260112174441.830780-3-sashal@kernel.org>
+
+From: Abdun Nihaal <nihaal@cse.iitm.ac.in>
+
+[ 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 <nihaal@cse.iitm.ac.in>
+Link: https://lore.kernel.org/r/20251226060414.20785-1-nihaal@cse.iitm.ac.in
+Signed-off-by: Bartosz Golaszewski <bartosz.golaszewski@oss.qualcomm.com>
+Signed-off-by: Sasha Levin <sashal@kernel.org>
+Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
+---
+ 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 = {
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