mm/swap, PM: hibernate: atomically replace hibernation pin

snapshot_set_swap_area() unpins the previously selected swap device and
pins the new one in two separate swap_lock critical sections.  In the gap
between them, swapoff() observes SWP_HIBERNATION cleared, bypasses the
guard, and tears down the device, reopening the race the SWP_HIBERNATION
pin was meant to close.  The window is reachable on any
SNAPSHOT_SET_SWAP_AREA call after the snapshot device is opened for
hibernation, and on any retry after the resume path's first selection.

Add repin_hibernation_swap_type(), which looks up the new device, clears
the old SWP_HIBERNATION flag and sets the new one under a single swap_lock
acquisition.  The same-device case is short- circuited so userspace can
re-select the same swap area without tripping WARN_ON_ONCE and -EBUSY. 
Switch snapshot_set_swap_area() to the new helper.

A failed lookup now preserves the previous pin instead of dropping it, so
a bad SNAPSHOT_SET_SWAP_AREA leaves the prior selection intact.  The open
and release paths keep using pin_hibernation_swap_type() and
unpin_hibernation_swap_type().

The race was identified during AI-assisted review of the SWP_HIBERNATION
pinning series.

[akpm@linux-foundation.org: coding-style cleanups]
Link: https://lore.kernel.org/20260430195651.287659-1-devnexen@gmail.com
Fixes: 8e6e0d845823 ("mm/swap, PM: hibernate: fix swapoff race in uswsusp by pinning swap device")
Signed-off-by: David Carlier <devnexen@gmail.com>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
Assisted-by: Codex (gpt-5-codex)
Cc: Baoquan He <bhe@redhat.com>
Cc: Barry Song <baohua@kernel.org>
Cc: Chris Li <chrisl@kernel.org>
Cc: Kairui Song <kasong@tencent.com>
Cc: Kemeng Shi <shikemeng@huaweicloud.com>
Cc: Len Brown <lenb@kernel.org>
Cc: Nhat Pham <nphamcs@gmail.com>
Cc: "Rafael J. Wysocki" <rafael@kernel.org>
Cc: DaeMyung Kang <charsyam@gmail.com>
Cc: Youngjun Park <youngjun.park@lge.com>
This commit is contained in:
David Carlier 2026-04-30 20:56:51 +01:00 committed by Andrew Morton
parent 50ee565c1e
commit 03825d869f
3 changed files with 68 additions and 12 deletions

View File

@ -395,6 +395,8 @@ static inline long get_nr_swap_pages(void)
extern void si_swapinfo(struct sysinfo *);
extern int pin_hibernation_swap_type(dev_t device, sector_t offset);
extern void unpin_hibernation_swap_type(int type);
extern int repin_hibernation_swap_type(int old_type, dev_t device,
sector_t offset);
extern int find_hibernation_swap_type(dev_t device, sector_t offset);
int find_first_swap(dev_t *device);
extern unsigned int count_swap_pages(int, int);

View File

@ -218,6 +218,7 @@ static int snapshot_set_swap_area(struct snapshot_data *data,
{
sector_t offset;
dev_t swdev;
int new_type;
if (swsusp_swap_in_use())
return -EPERM;
@ -238,19 +239,11 @@ static int snapshot_set_swap_area(struct snapshot_data *data,
offset = swap_area.offset;
}
/*
* Unpin the swap device if a swap area was already
* set by SNAPSHOT_SET_SWAP_AREA.
*/
unpin_hibernation_swap_type(data->swap);
new_type = repin_hibernation_swap_type(data->swap, swdev, offset);
if (new_type < 0)
return new_type;
/*
* User space encodes device types as two-byte values,
* so we need to recode them
*/
data->swap = pin_hibernation_swap_type(swdev, offset);
if (data->swap < 0)
return swdev ? -ENODEV : -EINVAL;
data->swap = new_type;
data->dev = swdev;
return 0;
}

View File

@ -2312,6 +2312,67 @@ int pin_hibernation_swap_type(dev_t device, sector_t offset)
return type;
}
/**
* repin_hibernation_swap_type - Atomically replace the hibernation pin
* @old_type: Swap type currently pinned (or < 0 if none).
* @device: Block device of the new resume image.
* @offset: Offset identifying the new swap area.
*
* Look up the swap device for @device/@offset and atomically transfer
* the SWP_HIBERNATION pin from @old_type (if valid) to the new device,
* all under a single swap_lock critical section. This closes the
* swapoff() window that exists when callers unpin and re-pin in two
* separate operations.
*
* If the new device cannot be located, the existing pin on @old_type
* is preserved and an error is returned. If @old_type already refers
* to the same swap_info_struct as the new lookup, no flag changes are
* made and @old_type is returned.
*
* Return:
* >= 0 on success (new swap type).
* -EINVAL if @device is invalid.
* -ENODEV if the swap device is not found.
* -EBUSY if the new device is already pinned by another context.
*/
int repin_hibernation_swap_type(int old_type, dev_t device, sector_t offset)
{
struct swap_info_struct *old_si, *new_si;
int new_type;
spin_lock(&swap_lock);
new_type = __find_hibernation_swap_type(device, offset);
if (new_type < 0) {
spin_unlock(&swap_lock);
return new_type;
}
new_si = swap_type_to_info(new_type);
if (WARN_ON_ONCE(!new_si)) {
spin_unlock(&swap_lock);
return -ENODEV;
}
old_si = swap_type_to_info(old_type);
if (new_si == old_si) {
spin_unlock(&swap_lock);
return new_type;
}
if (WARN_ON_ONCE(new_si->flags & SWP_HIBERNATION)) {
spin_unlock(&swap_lock);
return -EBUSY;
}
if (old_si)
old_si->flags &= ~SWP_HIBERNATION;
new_si->flags |= SWP_HIBERNATION;
spin_unlock(&swap_lock);
return new_type;
}
/**
* unpin_hibernation_swap_type - Unpin the swap device for hibernation
* @type: Swap type previously returned by pin_hibernation_swap_type()