[PATCH RFC 0/5] memcg: fix dying memcg pinned by swapped out shmem pages

Bingfang Guo via B4 Relay posted 5 patches 1 month, 2 weeks ago
include/linux/memcontrol.h |  11 +++-
mm/list_lru.c              |   2 +-
mm/memcontrol-v1.c         |  23 +++----
mm/memcontrol-v1.h         |   3 +-
mm/memcontrol.c            | 151 ++++++++++++++++++++++++++++++---------------
mm/workingset.c            |   6 +-
6 files changed, 126 insertions(+), 70 deletions(-)
[PATCH RFC 0/5] memcg: fix dying memcg pinned by swapped out shmem pages
Posted by Bingfang Guo via B4 Relay 1 month, 2 weeks ago
Currently the private ID of a memory cgroup holds a reference to its
css, so that users such as list_lru and swap entries can look the memcg
up by ID even after it has gone offline.

This has a downside: a memcg that still owns swapped out shmem pages is
pinned via its private ID and is never released until those pages are
swapped back in. Workloads that create and destroy many cgroups can
accumulate a large number of dying memcgs.

This series fixes that by moving the memcg private ID reference count
from the memcg to its object cgroup. The objcg is cheap to keep alive
and is reparented to an online ancestor when its memcg goes offline, so
holding the ID no longer pins the css.

Patch 1 (memcg: move memcg private ID refcount to objcg)

Move the ID refcount from the memcg to its objcg, so swapped out pages
no longer pin the dying memcg. The online state pins the objcg, and the
ID is released in css_released() after reparenting; this guarantees the
id refcount is alive for any caller holding a memcg reference. After
reparenting, mem_cgroup_from_private_id() returns a live ancestor
instead of the original memcg; callers that need the exact memcg are
fixed in patch 5.

Patch 2 (memcg: get stable memcg first before getting memcgid reference)

Both __memcg1_swapout() and __mem_cgroup_try_charge_swap() now take a
stable memcg reference first via get_mem_cgroup_from_objcg() and pin
the ID afterwards, dropping the rcu_read_lock() usage and the
get-error-put handling, and recording exactly the memcg the folio
belongs to in the swap entry.

Patch 3 (memcg: remove retry logic in mem_cgroup_private_id_get_online)

With the ID released only in css_released() (patch 1) and every caller
holding a stable reference (patch 2), the retry loop that walked up the
parent chain can never trigger. Remove it, rename the function to
mem_cgroup_private_id_get().

Patch 4 (memcg: add a helper to get online memcg from memcgid)

Add mem_cgroup_from_private_id_online(), which looks the ID up and
takes a reference through the objcg. Since the objcg is always
reparented to an online memcg, this guarantees an online memcg. Use it
in mem_cgroup_swapin_charge_folio(), dropping the manual id lookup and
css_tryget_online() check under the RCU read lock.

Patch 5 (memcg: filter out reparented memcgs got using memcgid)

After reparenting, mem_cgroup_from_private_id() can return a memcg that
no longer owns the ID. Callers such as list_lru and workingset refault
expect exactly the memcg the ID refers to, so check that the returned
memcg still owns the ID and return NULL otherwise, letting the callers
skip the entry.

The following program reproduces the problem: it repeatedly creates a
memcg, dirties one shared shmem page, pushes it to swap, and destroys
the cgroup. Each iteration leaves a dying memcg pinned by the swapped
out page.

shmem_dying_memcg.c:

```c

static int write_str(const char *path, const char *val) {
    int fd = open(path, O_WRONLY);
    if (fd < 0) return -1;
    int rc = write(fd, val, strlen(val)) < 0 ? -1 : 0;
    close(fd);
    return rc;
}

static long long dying_memcgs(void) {
    FILE *f = fopen(CG_ROOT "/cgroup.stat", "r");
    if (!f) return -1;
    char k[64];
    long long v = -1;
    while (fscanf(f, "%63s %lld", k, &v) == 2)
        if (!strcmp(k, "nr_dying_subsys_memory")) break;
    fclose(f);
    return v;
}

// worker: join the memcg, dirty one shared shmem page, swap it out, exit.
static void worker(const char *cgdir, const char *shmfile) {
    char pid[16];
    snprintf(pid, sizeof pid, "%d", getpid());
    if (write_str(cgdir, pid) < 0) _exit(1);           // cgroup.procs

    int fd = open(shmfile, O_CREAT | O_RDWR | O_TRUNC, 0644);
    if (fd < 0 || ftruncate(fd, PAGE) < 0) _exit(2);
    char *p = mmap(NULL, PAGE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (p == MAP_FAILED) _exit(3);
    close(fd);

    p[0] = 1;                                          // fault in + dirty
    if (madvise(p, PAGE, MADV_PAGEOUT) < 0) _exit(4);  // push to swap
    _exit(0);
}

static void one_iter(long i) {
    char cgdir[256], procs[300], shmfile[300];
    snprintf(cgdir,   sizeof cgdir,   "%s/m%ld", CG_BASE, i);
    snprintf(procs,   sizeof procs,   "%s/cgroup.procs", cgdir);
    snprintf(shmfile, sizeof shmfile, "%s/f%ld", SHM_DIR, i);

    if (mkdir(cgdir, 0755) < 0 && errno != EEXIST) return;

    pid_t pid = fork();
    if (pid == 0) worker(procs, shmfile);
    waitpid(pid, NULL, 0);

    rmdir(cgdir);   // memcg goes dying; swapped page pins its id
}

static void cleanup(void) {
    struct dirent *de;
    char p[512];
    DIR *d = opendir(CG_BASE);
    if (d) {
        while ((de = readdir(d)))
            if (de->d_name[0] == 'm') {
                snprintf(p, sizeof p, "%s/%s", CG_BASE, de->d_name);
                rmdir(p);
            }
        closedir(d);
    }
    d = opendir(SHM_DIR);
    if (d) {
        while ((de = readdir(d)))
            if (de->d_name[0] != '.') {
                snprintf(p, sizeof p, "%s/%s", SHM_DIR, de->d_name);
                unlink(p);
            }
        closedir(d);
    }
    rmdir(CG_BASE);
    rmdir(SHM_DIR);
    printf("cleanup done; nr_dying_subsys_memory = %lld\n", dying_memcgs());
}

int main(int argc, char **argv) {
    if (geteuid() != 0) { fprintf(stderr, "run as root\n"); return 1; }

    if (argc > 1 && !strcmp(argv[1], "--cleanup")) { cleanup(); return 0; }
    long n = argc > 1 ? atol(argv[1]) : 1000;

    mkdir(CG_BASE, 0755);
    write_str(CG_ROOT "/cgroup.subtree_control", "+memory");
    if (write_str(CG_BASE "/cgroup.subtree_control", "+memory") < 0) {
        fprintf(stderr, "enable +memory: %s\n", strerror(errno));
        return 1;
    }
    mkdir(SHM_DIR, 0755);

    printf("before: nr_dying_subsys_memory = %lld\n", dying_memcgs());
    for (long i = 0; i < n; i++) one_iter(i);
    printf("after:  nr_dying_subsys_memory = %lld  (created %ld memcgs)\n",
           dying_memcgs(), n);
    printf("release with: sudo %s --cleanup\n", argv[0]);
    return 0;
}
```

Run with:

```bash
echo "=== before ==="
grep dying /sys/fs/cgroup/cgroup.stat

sudo ./shmem_dying_memcg "$@"

sleep 1

echo
echo "=== after (root cgroup.stat) ==="
grep dying /sys/fs/cgroup/cgroup.stat
```

To release the pinned memcgs:
$ sudo ./shmem_dying_memcg --cleanup

Results on my machine (20000 iterations):

Before this series, all 20000 destroyed memcgs stay dying:
```
=== before ===
nr_dying_descendants 20002
nr_dying_subsys_cpuset 0
nr_dying_subsys_cpu 0
nr_dying_subsys_io 2
nr_dying_subsys_memory 20002
nr_dying_subsys_perf_event 0
nr_dying_subsys_hugetlb 0
nr_dying_subsys_pids 0
before: nr_dying_subsys_memory = 20002
after:  nr_dying_subsys_memory = 20002  (created 20000 memcgs)
release with: sudo ./shmem_dying_memcg --cleanup

=== after (root cgroup.stat) ===
nr_dying_descendants 20002
nr_dying_subsys_cpuset 0
nr_dying_subsys_cpu 0
nr_dying_subsys_io 2
nr_dying_subsys_memory 20002
nr_dying_subsys_perf_event 0
nr_dying_subsys_hugetlb 0
nr_dying_subsys_pids 0
```

After this series, dying memcgs no longer accumulate:
```
=== before ===
nr_dying_descendants 0
nr_dying_subsys_cpuset 0
nr_dying_subsys_cpu 0
nr_dying_subsys_io 0
nr_dying_subsys_memory 0
nr_dying_subsys_perf_event 0
nr_dying_subsys_hugetlb 0
nr_dying_subsys_pids 0
before: nr_dying_subsys_memory = 0
after:  nr_dying_subsys_memory = 314  (created 20000 memcgs)
release with: sudo ./shmem_dying_memcg --cleanup

=== after (root cgroup.stat) ===
nr_dying_descendants 0
nr_dying_subsys_cpuset 0
nr_dying_subsys_cpu 0
nr_dying_subsys_io 0
nr_dying_subsys_memory 0
nr_dying_subsys_perf_event 0
nr_dying_subsys_hugetlb 0
nr_dying_subsys_pids 0
```

Signed-off-by: Bingfang Guo <bingfangguo@tencent.com>
---
Bingfang Guo (5):
      memcg: move memcg private ID refcount to objcg
      memcg: get stable memcg first before getting memcgid reference
      memcg: remove retry logic in mem_cgroup_private_id_get_online
      memcg: add a helper to get online memcg from memcgid
      memcg: filter out reparented memcgs got using memcgid

 include/linux/memcontrol.h |  11 +++-
 mm/list_lru.c              |   2 +-
 mm/memcontrol-v1.c         |  23 +++----
 mm/memcontrol-v1.h         |   3 +-
 mm/memcontrol.c            | 151 ++++++++++++++++++++++++++++++---------------
 mm/workingset.c            |   6 +-
 6 files changed, 126 insertions(+), 70 deletions(-)
---
base-commit: 288058d8db5d35623228d84f48d9bea3707d5c85
change-id: 20260812-memcgid-objcg-de9f578ef128

Best regards,
-- 
Bingfang Guo <bingfangguo@tencent.com>
[syzbot ci] Re: memcg: fix dying memcg pinned by swapped out shmem pages
Posted by syzbot ci 1 month, 2 weeks ago
syzbot ci has tested the following series

[v1] memcg: fix dying memcg pinned by swapped out shmem pages
https://lore.kernel.org/all/20260813-memcgid-objcg-v1-0-83d21c685b77@tencent.com
* [PATCH RFC 1/5] memcg: move memcg private ID refcount to objcg
* [PATCH RFC 2/5] memcg: get stable memcg first before getting memcgid reference
* [PATCH RFC 3/5] memcg: remove retry logic in mem_cgroup_private_id_get_online
* [PATCH RFC 4/5] memcg: add a helper to get online memcg from memcgid
* [PATCH RFC 5/5] memcg: filter out reparented memcgs got using memcgid

and found the following issue:
WARNING: refcount bug in __mem_cgroup_uncharge_swap

Full report is available here:
https://ci.syzbot.org/series/3a8a2a5f-8b6d-4abf-954e-a1a18ee62748

***

WARNING: refcount bug in __mem_cgroup_uncharge_swap

tree:      linux-next
URL:       https://kernel.googlesource.com/pub/scm/linux/kernel/git/next/linux-next
base:      288058d8db5d35623228d84f48d9bea3707d5c85
arch:      amd64
compiler:  Debian clang version 22.1.8 (++20260613092233+e80beda6e255-1~exp1~20260613092250.77), Debian LLD 22.1.8
config:    https://ci.syzbot.org/builds/35dac6f6-b44a-4ea8-9adf-2cdda5584e45/config
syz repro: https://ci.syzbot.org/findings/6e117c0b-d6e2-44a6-8e35-161cb33f2aff/syz_repro

------------[ cut here ]------------
refcount_t: underflow; use-after-free.
WARNING: lib/refcount.c:28 at refcount_warn_saturate+0xb2/0x110 lib/refcount.c:28, CPU#0: syz.2.19/5863
Modules linked in:
CPU: 0 UID: 0 PID: 5863 Comm: syz.2.19 Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
RIP: 0010:refcount_warn_saturate+0xb2/0x110 lib/refcount.c:28
Code: 64 84 8e 0b 67 48 0f b9 3a eb 4a e8 58 fa f2 fc 48 8d 3d 61 84 8e 0b 67 48 0f b9 3a eb 37 e8 45 fa f2 fc 48 8d 3d 5e 84 8e 0b <67> 48 0f b9 3a eb 24 e8 32 fa f2 fc 48 8d 3d 5b 84 8e 0b 67 48 0f
RSP: 0018:ffffc90003bbf1d0 EFLAGS: 00010293
RAX: ffffffff84d3ed9b RBX: 0000000000000003 RCX: ffff8881bdf58000
RDX: 0000000000000000 RSI: ffffffff8f363380 RDI: ffffffff90627200
RBP: 1ffffffff20ae968 R08: ffff8881bdf58000 R09: 0000000000000005
R10: 0000000000000004 R11: 0000000000000000 R12: ffff8881026f8000
R13: ffff88810a914100 R14: ffff88810a914134 R15: 1ffff110204df009
FS:  00007fe2ee63d6c0(0000) GS:ffff88818d960000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 000056066247f0b8 CR3: 000000000e946000 CR4: 00000000000006f0
Call Trace:
 <TASK>
 mem_cgroup_take_from_private_id mm/memcontrol.c:4049 [inline]
 __mem_cgroup_uncharge_swap+0x204/0x2a0 mm/memcontrol.c:5707
 mem_cgroup_uncharge_swap include/linux/swap.h:520 [inline]
 __swap_cluster_free_entries+0x735/0xc50 mm/swapfile.c:1955
 swap_put_entries_cluster+0x3b1/0x4b0 mm/swapfile.c:1629
 swap_put_entries_direct+0x137/0x210 mm/swapfile.c:2139
 zap_nonpresent_ptes mm/memory.c:1838 [inline]
 do_zap_pte_range mm/memory.c:1905 [inline]
 zap_pte_range mm/memory.c:2003 [inline]
 zap_pmd_range mm/memory.c:2089 [inline]
 zap_pud_range mm/memory.c:2117 [inline]
 zap_p4d_range mm/memory.c:2138 [inline]
 __zap_vma_range+0x1d9d/0x4f10 mm/memory.c:2178
 unmap_vmas+0x390/0x550 mm/memory.c:2247
 exit_mmap+0x293/0x9f0 mm/mmap.c:1315
 __mmput+0x118/0x420 kernel/fork.c:1187
 exit_mm+0x221/0x2d0 kernel/exit.c:615
 do_exit+0x6cd/0x2360 kernel/exit.c:997
 do_group_exit+0x22d/0x2f0 kernel/exit.c:1152
 get_signal+0x121b/0x12c0 kernel/signal.c:3046
 arch_do_signal_or_restart+0xbb/0x860 arch/x86/kernel/signal.c:337
 __exit_to_user_mode_loop kernel/entry/common.c:66 [inline]
 exit_to_user_mode_loop+0x104/0x730 kernel/entry/common.c:101
 __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
 syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
 syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
 do_syscall_64+0x353/0x580 arch/x86/entry/syscall_64.c:100
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7fe2ed79e0d9
Code: Unable to access opcode bytes at 0x7fe2ed79e0af.
RSP: 002b:00007fe2ee63d0e8 EFLAGS: 00000246 ORIG_RAX: 00000000000000ca
RAX: fffffffffffffe00 RBX: 00007fe2eda25fa8 RCX: 00007fe2ed79e0d9
RDX: 0000000000000000 RSI: 0000000000000080 RDI: 00007fe2eda25fa8
RBP: 00007fe2eda25fa0 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007fe2eda26038 R14: 00007fff0bed41b0 R15: 00007fff0bed4298
 </TASK>
----------------
Code disassembly (best guess):
   0:	64 84 8e 0b 67 48 0f 	test   %cl,%fs:0xf48670b(%rsi)
   7:	b9 3a eb 4a e8       	mov    $0xe84aeb3a,%ecx
   c:	58                   	pop    %rax
   d:	fa                   	cli
   e:	f2 fc                	repnz cld
  10:	48 8d 3d 61 84 8e 0b 	lea    0xb8e8461(%rip),%rdi        # 0xb8e8478
  17:	67 48 0f b9 3a       	ud1    (%edx),%rdi
  1c:	eb 37                	jmp    0x55
  1e:	e8 45 fa f2 fc       	call   0xfcf2fa68
  23:	48 8d 3d 5e 84 8e 0b 	lea    0xb8e845e(%rip),%rdi        # 0xb8e8488
* 2a:	67 48 0f b9 3a       	ud1    (%edx),%rdi <-- trapping instruction
  2f:	eb 24                	jmp    0x55
  31:	e8 32 fa f2 fc       	call   0xfcf2fa68
  36:	48 8d 3d 5b 84 8e 0b 	lea    0xb8e845b(%rip),%rdi        # 0xb8e8498
  3d:	67                   	addr32
  3e:	48                   	rex.W
  3f:	0f                   	.byte 0xf


***

If these findings have caused you to resend the series or submit a
separate fix, please add the following tag to your commit message:
  Tested-by: syzbot@syzkaller.appspotmail.com

---
This report is generated by a bot. It may contain errors.
syzbot ci engineers can be reached at syzkaller@googlegroups.com.

To test a fix for this bug, please reply with `#syz test`
(on a separate line) and attach the patch to the email.

Notes:
- The patch will be applied on top of the tested series (as an
  incremental fix).
- To test a new version of the whole series, please send it directly
  to syzbot@lists.linux.dev.
- Arguments like custom git repos and branches are not supported.