]> git.ipfire.org Git - thirdparty/kernel/stable-queue.git/commitdiff
6.18-stable patches
authorGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Thu, 15 Jan 2026 10:26:51 +0000 (11:26 +0100)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Thu, 15 Jan 2026 10:26:51 +0000 (11:26 +0100)
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

queue-6.18/btrfs-fix-beyond-eof-write-handling.patch [new file with mode: 0644]
queue-6.18/btrfs-truncate-ordered-extent-when-skipping-writeback-past-i_size.patch [new file with mode: 0644]
queue-6.18/btrfs-use-variable-for-end-offset-in-extent_writepage_io.patch [new file with mode: 0644]
queue-6.18/gpio-mpsse-add-quirk-support.patch [new file with mode: 0644]
queue-6.18/gpio-mpsse-ensure-worker-is-torn-down.patch [new file with mode: 0644]
queue-6.18/gpio-mpsse-fix-reference-leak-in-gpio_mpsse_probe-error-paths.patch [new file with mode: 0644]
queue-6.18/series

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 (file)
index 0000000..c02dbb2
--- /dev/null
@@ -0,0 +1,155 @@
+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)) {
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 (file)
index 0000000..2e6d063
--- /dev/null
@@ -0,0 +1,248 @@
+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;
+ }
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 (file)
index 0000000..f73c4ef
--- /dev/null
@@ -0,0 +1,74 @@
+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);
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 (file)
index 0000000..22e56ac
--- /dev/null
@@ -0,0 +1,246 @@
+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)
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 (file)
index 0000000..c48ff99
--- /dev/null
@@ -0,0 +1,209 @@
+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);
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 (file)
index 0000000..77d1e42
--- /dev/null
@@ -0,0 +1,63 @@
+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 = {
index c8b4bc4e67c559122776f0e583542a8d65e51b4e..252f790d3d2a920450572b6b8367cb7dd5e9b623 100644 (file)
@@ -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