Re: [RFC PATCH] zram: avoid preemption with CPU-based compression backends

From: Bo Zhang

Date: Tue Aug 04 2026 - 22:21:27 EST


On Wed, Aug 05, 2026 at 08:55:45AM +0800, Barry Song (Xiaomi) wrote:
> Since commit 2efa9e9eb4db ("zram: permit preemption with active
> compression stream"), a major Android regression has been reported.
>
> We add an async flag (currently false for almost all backends) to
> indicate whether a backend is asynchronous. For synchronous
> backends, we use preempt_disable() in the !PREEMPT_RT case. A
> zram_zs_malloc() wrapper is provided to support a two-stage
> zs_malloc() path, allowing allocation to transition from a
> non-sleepable context to a sleepable context.

Hi Barry,

Thanks for working on this. The priority inversion issue on Android is
real and we've hit it too.

However, I think there's a bug in the zcomp_stream_put() change.

The new zcomp_stream_put() uses raw_cpu_ptr() to find the stream:

void zcomp_stream_put(struct zcomp *comp)
{
struct zcomp_strm *zstrm = raw_cpu_ptr(comp->stream);
if (!comp->ops->async && !IS_ENABLED(CONFIG_PREEMPT_RT))
preempt_enable();
mutex_unlock(&zstrm->lock);
}

It relies on the thread still being on the same CPU where
zcomp_stream_get() locked the stream. In the normal path it works
fine, because preempt is disabled the entire time.

But zram_zs_malloc() breaks this assumption in the fallback path:

static unsigned long zram_zs_malloc(...)
{
if (!async && !IS_ENABLED(CONFIG_PREEMPT_RT)) {
handle = zs_malloc(..., __GFP_KSWAPD_RECLAIM | ...);
if (!IS_ERR_VALUE(handle))
return handle;
preempt_enable(); // preempt is now enabled
}

handle = zs_malloc(..., GFP_NOIO | ...); // may sleep, may migrate
if (!async && !IS_ENABLED(CONFIG_PREEMPT_RT))
preempt_disable(); // Preempt disabled, but it may work on another CPU
return handle;
}

If the first zs_malloc() fails, preempt is enabled and the second
zs_malloc(GFP_NOIO) can sleep. During this window the task can migrate
to another CPU. After preempt_disable(), we continue on the new CPU.

When zcomp_stream_put() is later called, raw_cpu_ptr() returns the new
CPU's stream, which is not the one that was originally locked. Which may
cause a mutex_unlock() on the wrong CPU.

The original code didn't have this problem because zcomp_stream_put()
took the zstrm pointer directly:

void zcomp_stream_put(struct zcomp_strm *zstrm)
{
mutex_unlock(&zstrm->lock);
}

The caller always passed the saved pointer from zcomp_stream_get(),
so regardless of CPU migration, the correct mutex was always unlocked.

I think the fix is to keep passing the zstrm pointer:

void zcomp_stream_put(struct zcomp *comp, struct zcomp_strm *zstrm)
{
if (!comp->ops->async && !IS_ENABLED(CONFIG_PREEMPT_RT))
preempt_enable();
mutex_unlock(&zstrm->lock);
}

All callers already have zstrm available. Then It works as expected.

Bo