l9ec: The Magic Linux Kernel Patch

Since 2007, a Linux kernel bug has caused full system freezes under heavy memory pressure — and mainstream developers still refuse to fix it. This article documents porting the unofficial l9ec working-set protection patch to Xanmod kernel 6.14.5.

This article is for those who desperately want to use older hardware for modern development. "Machines should serve, not demand resources" — and the author of the l9 patch understands this principle deeply.

l9ec patch overview

The Epic Bug

Since 2007, a serious bug has existed in the Linux kernel causing complete system freezes under heavy memory load. As of May 2025, this bug has officially come of age — it is 18 years old.

The original bug report documented system lockups under memory pressure. While kernel developers are aware of the issue, they do not consider it important enough to fix. The bug tracker was eventually closed with the comment "just become obsolete," implying users should stop "building computers from garbage heaps" and instead use "at least 32GB of RAM for a desktop."

However, the problem extends far beyond vintage computer enthusiasts. Modern Linux deployments typically run on virtual machines with limited memory. A typical corporate website likely runs on a VM with 4GB RAM. This means practically all Linux users face this issue, not just retro-computing hobbyists.

System freeze under memory pressure

Root Cause

The bug manifests when memory pressure is gradually increased through small allocations from multiple processes, preventing the OOM Killer from responding. This commonly occurs during:

  • Neural network training
  • Running heavy Java or Node.js applications, especially IDEs
  • Building large projects on underpowered hardware or VMs

The l9ec Patch

An unofficial patch solves this through "radical, straightforward means": protecting a small memory region — the "working set" — that applications cannot overload, preventing thrashing and livelock.

A year-long discussion on the Linux Kernel mailing list followed the patch's publication, but it never reached mainline, raising serious questions about kernel development priorities.

LKML discussion

Xanmod and MGLRU

Xanmod, a community kernel distribution emphasizing desktop responsiveness, initially included the l9 patch. However, recent versions removed it, citing the emergence of MGLRU (Multi-Generational LRU) as supposedly solving memory pressure issues.

Unfortunately, MGLRU follows different principles and was tested on Android with different workloads — "realistic user behavior" — not heavy development tools running on resource-constrained systems.

The patch author eventually abandoned porting to kernel 6.x, assuming MGLRU made it obsolete. However, the l9 patch is more predictable and reliable than the "14-patch MGLRU circus." This article documents porting l9ec-5.15.patch to Xanmod kernel 6.14.5.

Kernel patch comparison

Porting l9ec to Kernel 6.x

Four files require changes:

  1. include/linux/mm.h
  2. kernel/sysctl.c
  3. mm/Kconfig
  4. mm/vmscan.c

Step 1 — include/linux/mm.h:

Add the three extern declarations for the new sysctl variables:

extern unsigned long sysctl_anon_min_kbytes;
extern unsigned long sysctl_clean_low_kbytes;
extern unsigned long sysctl_clean_min_kbytes;

Step 2 — kernel/sysctl.c:

Add three entries to static struct ctl_table vm_table[]:

{
    .procname   = "anon_min_kbytes",
    .data       = &sysctl_anon_min_kbytes,
    .maxlen     = sizeof(unsigned long),
    .mode       = 0644,
    .proc_handler   = proc_doulongvec_minmax,
},
{
    .procname   = "clean_low_kbytes",
    .data       = &sysctl_clean_low_kbytes,
    .maxlen     = sizeof(unsigned long),
    .mode       = 0644,
    .proc_handler   = proc_doulongvec_minmax,
},
{
    .procname   = "clean_min_kbytes",
    .data       = &sysctl_clean_min_kbytes,
    .maxlen     = sizeof(unsigned long),
    .mode       = 0644,
    .proc_handler   = proc_doulongvec_minmax,
},

Step 3 — mm/Kconfig:

Add ANON_MIN_KBYTES, CLEAN_LOW_KBYTES, and CLEAN_MIN_KBYTES configuration options with their default values and help text.

Step 4 — mm/vmscan.c:

Add variable declarations and initialization:

unsigned long sysctl_anon_min_kbytes __read_mostly = CONFIG_ANON_MIN_KBYTES;
unsigned long sysctl_clean_low_kbytes __read_mostly = CONFIG_CLEAN_LOW_KBYTES;
unsigned long sysctl_clean_min_kbytes __read_mostly = CONFIG_CLEAN_MIN_KBYTES;

Implement the prepare_workingset_protection() function:

static void prepare_workingset_protection(pg_data_t *pgdat,
                                          struct scan_control *sc)
{
    if (sysctl_anon_min_kbytes) {
        unsigned long reclaimable_anon;

        reclaimable_anon =
            node_page_state(pgdat, NR_ACTIVE_ANON) +
            node_page_state(pgdat, NR_INACTIVE_ANON) +
            node_page_state(pgdat, NR_ISOLATED_ANON);
        reclaimable_anon <<= (PAGE_SHIFT - 10);

        sc->anon_below_min = reclaimable_anon < sysctl_anon_min_kbytes;
    } else
        sc->anon_below_min = 0;

    if (sysctl_clean_low_kbytes || sysctl_clean_min_kbytes) {
        unsigned long reclaimable_file, dirty, clean;

        reclaimable_file =
            node_page_state(pgdat, NR_ACTIVE_FILE) +
            node_page_state(pgdat, NR_INACTIVE_FILE) +
            node_page_state(pgdat, NR_ISOLATED_FILE);
        dirty = node_page_state(pgdat, NR_FILE_DIRTY);

        if (likely(reclaimable_file > dirty))
            clean = (reclaimable_file - dirty) << (PAGE_SHIFT - 10);
        else
            clean = 0;

        sc->clean_below_low = clean < sysctl_clean_low_kbytes;
        sc->clean_below_min = clean < sysctl_clean_min_kbytes;
    } else {
        sc->clean_below_low = 0;
        sc->clean_below_min = 0;
    }
}

In get_scan_count(), add the working-set protection check:

if (sc->clean_below_low || sc->clean_below_min) {
    scan_balance = SCAN_ANON;
    goto out;
}

And the hard-protection block per LRU list:

if (file) {
    if (sc->clean_below_min)
        scan = 0;
} else {
    if (sc->anon_below_min)
        scan = 0;
}
nr[lru] = scan;
vmscan.c changes

Building and Disabling MGLRU

make xconfig
make && make modules && make modules_install && make install

After boot, disable MGLRU to let the l9ec patch take full effect:

cat /sys/kernel/mm/lru_gen/enabled
echo 0 | sudo tee /sys/kernel/mm/lru_gen/enabled
MGLRU disabled

Test Results

Testing was performed on a 2012 Lenovo Z580 with 8GB RAM, running simultaneously:

  • PostgreSQL and MySQL with real production databases
  • IntelliJ IDEA
  • VSCode
  • Node.js build with Webpack hot reload
  • A large Java project build (~3000 files)
  • Chromium with 20 open tabs

All workloads completed without any freeze. Subsequent testing on a 2007 laptop with only 3GB RAM also succeeded.

Test machine

Conclusion

The author intentionally uses old hardware to assess software efficiency — and thereby produces more efficient tools. This problem affects virtually all Linux users, not just vintage hardware enthusiasts — in particular, CI/CD environments running multiple simultaneous builds on memory-constrained VMs.

The l9ec patch's simple, predictable solution consistently outperforms more complex alternatives. Respect goes to the original patch author, whose work represents the best of practical open-source engineering.