https://bugs.kde.org/show_bug.cgi?id=523770

            Bug ID: 523770
           Summary: Use-after-free in DpmsInputEventFilter::touchUp: the
                    filter writes to its own members after notify() has
                    destroyed it
    Classification: Plasma
           Product: kwin
      Version First 6.6.6
       Reported In:
          Platform: Kubuntu
                OS: Linux
            Status: REPORTED
          Severity: crash
          Priority: NOR
         Component: input
          Assignee: [email protected]
          Reporter: [email protected]
                CC: [email protected]
  Target Milestone: ---

Created attachment 194798
  --> https://bugs.kde.org/attachment.cgi?id=194798&action=edit
Full backtrace and heap state from one of the core dumps

Still present in current master. The crashes reported here were observed on
6.6.5 and
6.6.6, which is what the core dumps below come from. The offending code is
byte-identical
in current master (6.7.80) — the sources quoted here are taken from master, not
from the
version the dumps came from.

SUMMARY

DpmsInputEventFilter::touchUp() calls notify() and then assigns to two of its
own
members. notify() destroys the filter synchronously, so both assignments write
into freed memory. On a touch-only system this happens on every double-tap
wake-up and eventually corrupts the heap.

    bool DpmsInputEventFilter::touchUp(TouchUpEvent *event)
    {
        if (m_enableDoubleTap) {
            m_touchPoints.removeAll(event->id);
            if (m_touchPoints.isEmpty() && m_doubleTapTimer.isValid() &&
m_secondTap) {
                // if device in pocket, do not wake device up
                if (m_doubleTapTimer.elapsed() < qApp->doubleClickInterval() &&
!m_proximityClose) {
                    notify();                    // destroys *this*
                }
                m_doubleTapTimer.invalidate();   // write to freed memory
                m_secondTap = false;             // write to freed memory
            }
        }
        return true;
    }

The destruction is entirely synchronous, within one call stack:

    DpmsInputEventFilter::notify()
      -> Workspace::requestDpmsState(DpmsState::On)
         -> m_dpmsFilter.reset()
            -> ~DpmsInputEventFilter()
               -> ~InputEventFilter()
                  -> InputRedirection::uninstallInputEventFilter(this)
                  -> operator delete

The other handlers are fine: pointerMotion, pointerButton, pointerAxis and
keyboardKey call notify() and return immediately without touching any member.
touchUp is the only one that keeps using the object.


STEPS TO REPRODUCE

1. Wayland session on a system whose only input device is a touchscreen, with
   DoubleTapWakeup at its default of true.
2. Let power management switch the outputs off via DPMS.
3. Wake the screen with a double tap.

Every such wake-up performs two writes into freed memory.

Why this mostly affects touch-only systems: while the outputs are off, a single
tap does not wake them, because touchDown/touchUp only accumulate touch points
and return true. The double tap is the only way to wake the screen through the
touchscreen, so on a device without keyboard or mouse the only available
wake-up gesture is exactly the broken code path. On a desktop the user nudges
the mouse instead, and pointerMotion is safe.


OBSERVED RESULT

The stray writes usually land in unused freed memory and do nothing. Only when
that region has been handed out again do they overwrite live heap metadata.
glibc then aborts at the next allocation that touches the damaged chunk -
minutes to days later, in an entirely unrelated code path. With the default
configuration this took roughly 6 to 9 days per machine.

Two aborts from the same installation, showing how far the reported location is
from the actual cause:

    malloc_printerr ("corrupted size vs. prev_size")
    unlink_chunk / _int_malloc / __libc_malloc2 / operator new
      KWin::Transaction::watchDmaBuf
      KWin::Transaction::commit
      wl_event_loop_dispatch
      KWin::Display::dispatchEvents

    malloc_printerr ("corrupted size vs. prev_size")
    unlink_chunk / _int_malloc / realloc / QArrayData::reallocateUnaligned
      KWin::ItemRendererOpenGL::createRenderNode
      KWin::ItemRendererOpenGL::renderItem
      KWin::WorkspaceScene::paint
      KWin::Compositor::composite

Anyone hitting this will report it as a random compositing or rendering crash.
Searching Bugzilla for "corrupted size vs. prev_size" in kwin returns nothing,
which fits the real cause never having been identified.


EXPECTED RESULT

No write to the object after notify() has destroyed it, and no crash.


SOFTWARE/OS VERSIONS

KDE Plasma: 6.6.5 and 6.6.6 (both affected, core dumps from both)
KWin: still affected in current master (6.7.80) - the code quoted above is
taken
      from master, not from the versions that produced the dumps
Qt: 6.10.2
Operating System: Linux, Wayland session


ADDITIONAL INFORMATION

Evidence from core dumps
------------------------

Three core dumps from two different machines (different CPU generation,
chipset,
GPU and display connector), kwin 6.6.5 and 6.6.6. In every one of them:

1. Exactly one chunk in the whole heap has a corrupted prev_size. The heap is
   otherwise fully walkable, around 150000 chunks from start to end.
2. That chunk is free and still contains a destroyed InputEventFilter.
3. The object's vptr is the vtable of InputEventFilter itself, not of a derived
   class - the destructor chain had reached the base when the memory was
   released.
4. m_weight == 1 == InputFilterOrder::Dpms identifies it as the DPMS filter.
5. Its QElapsedTimer members hold 0x8000000000000000 twice, which is precisely
   what QElapsedTimer::invalidate() writes. That is the post-free write itself,
   preserved in the dump.

Example from the 6.6.6 dump, object at offset 0x90 of a free 0xd0-byte chunk:

    +0x00  0x00007f........      vtable for KWin::InputEventFilter
    +0x08  0x0032000000000001    m_weight = 1  (InputFilterOrder::Dpms)
    +0x10  0x8000000000000000    QElapsedTimer t1  } written by invalidate()
    +0x18  0x8000000000000000    QElapsedTimer t2  } after the free

This can also be observed without waiting for a crash: right after a double-tap
wake-up, walk kwin's heap and look for a free chunk that still contains an
InputEventFilter whose m_weight is 1 and whose two timer fields both read
0x8000000000000000. An ASan build should flag the write immediately.


Secondary issue in the same area
--------------------------------

InputRedirection::processFilters() iterates over m_filters itself while a
filter
is allowed to destroy itself - and ~InputEventFilter removes it from that very
list:

    void processFilters(auto method, const auto &...args)
    {
        for (const auto filter : std::as_const(m_filters)) {
            if ((filter->*method)(args...)) {
                return;
            }
        }
    }

This stays benign only because every handler that calls notify() returns true,
which ends the loop at once - with one exception. DpmsInputEventFilter::
switchEvent() calls notify() on a lid switch and then returns false, so the
loop
continues over a list that was just modified underneath it.


Proposed fix
------------

Two changes, kept separate so the first can be taken on its
own:

1. dpmsinputeventfilter: reset the filter's state before calling notify(), so
   that notify() is the last statement touching the object.
2. input: make processFilters tolerate filters destroyed during dispatch, by
   iterating over a copy and skipping filters that are no longer installed.

-- 
You are receiving this mail because:
You are watching all bug changes.

Reply via email to