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

            Bug ID: 524567
           Summary: convertToQImage writes through a null pointer when an
                    ABR brush declares out-of-range bounds
    Classification: Applications
           Product: krita
      Version First 6.0.3
       Reported In:
          Platform: Ubuntu
                OS: Linux
            Status: REPORTED
          Severity: crash
          Priority: NOR
         Component: Resource Management
          Assignee: [email protected]
          Reporter: [email protected]
  Target Milestone: ---

Created attachment 195358
  --> https://bugs.kde.org/attachment.cgi?id=195358&action=edit
07_abr_convertToQImage_line48.bin       51-byte ABR brush file — null
dereference in kis_abr_brush_collection.cpp:48

convertToQImage writes through a null pointer when an ABR brush declares
out-of-range bounds

Opening an ABR brush file whose bounds fields give a width and height that
QImage cannot allocate crashes Krita: `abr_brush_load_v12()` derives the
dimensions from the file's `left`/`right`/`top`/`bottom` and passes them to
`convertToQImage()` with only an upper bound on the height checked, the
`QImage` built there is null, and the pixel loop stores through the null
pointer `QImage::scanLine()` hands back.

## error log

```
AddressSanitizer:DEADLYSIGNAL
=================================================================
==27==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc
0x7f7107ada25b bp 0x7ffcdbc0fe10 sp 0x7ffcdbc0fd20 T0)
==27==The signal is caused by a WRITE memory access.
==27==Hint: address points to the zero page.
    #0 0x7f7107ada25b in convertToQImage(char*, int, int)
/w/krita-6.0.3/libs/brush/kis_abr_brush_collection.cpp:48:22
    #1 0x7f7107ada25b in
KisAbrBrushCollection::abr_brush_load_v12(QDataStream&, AbrInfo*, QString, int,
int) /w/krita-6.0.3/libs/brush/kis_abr_brush_collection.cpp:474:36
    #2 0x7f7107adc507 in KisAbrBrushCollection::abr_brush_load(QDataStream&,
AbrInfo*, QString, int, int)
/w/krita-6.0.3/libs/brush/kis_abr_brush_collection.cpp:511:20
    #3 0x7f7107ae12f1 in KisAbrBrushCollection::loadFromDevice(QIODevice*)
/w/krita-6.0.3/libs/brush/kis_abr_brush_collection.cpp:593:20
    #4 0x558103a41bca in main /w/harness/h_brush.cpp:92:14
    #5 0x7f71033a71c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId:
328820b908de8ea1ef79afa8995e302e819163d7)
    #6 0x7f71033a728a in __libc_start_main
(/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId:
328820b908de8ea1ef79afa8995e302e819163d7)
    #7 0x5581039655e4 in _start (/w/harness/h_brush+0x2e5e4) (BuildId:
3d4bca5d48c4bd96e8648f4826cd656ad8b103a1)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV
/w/krita-6.0.3/libs/brush/kis_abr_brush_collection.cpp:48:22 in
convertToQImage(char*, int, int)
==27==ABORTING
```

That is the whole report; the sanitizer had nothing to add for a fault at
address 0.

## context

* Krita 6.0.3, release tarball `krita-6.0.3.tar.gz`, sha256
`06397997a3b6700d5167068dc0f50cc356d82018611ed37087a1566790b721a8`, which is
tag `v6.0.3` (858d352). All line numbers below are that source's; they were not
re-checked against the development branch.
* Affected file: `libs/brush/kis_abr_brush_collection.cpp`.
* Build: CMake, `-DCMAKE_BUILD_TYPE=RelWithDebInfo`, `-DBUILD_WITH_QT6=OFF`,
`-DBUILD_TESTING=OFF`, `-DBUILD_COVERAGE=OFF`, `-DENABLE_CLAZY=OFF`,
`-DENABLE_BSYMBOLICFUNCTIONS=OFF`, `CMAKE_CXX_FLAGS='-fno-omit-frame-pointer
-g'`.
* Platform: x86_64 Linux, Ubuntu 24.04, Qt 5.15.13, KF5 5.115.
* Two builds were used: one built through AFL++'s clang wrapper with
`AFL_USE_ASAN=1`, which produced the output above, and one built with
`/usr/bin/clang++` with neither the address sanitizer nor coverage
instrumentation, used for confirmation.

## how to reproduce

1. Build Krita 6.0.3 as above and compile the program below against
`libs/brush`. It is the whole test program, quoted here in full apart from its
header comment: it defines no Krita code, and
`KisAbrBrushCollection::loadFromDevice()` is the same call Krita makes on an
`.abr` file, from `KisAbrBrushCollection::load()` at
`kis_abr_brush_collection.cpp:555`, which is what `KisAbrStorage` drives. Add
`-fno-operator-names` to the compile line; Krita's own compile lines carry it
and the program does not build against the Qt headers without it.

```cpp
#include <QtGlobal>
#include <QGuiApplication>
#include <QBuffer>
#include <QByteArray>
#include <QFile>
#include <QString>

#include <kis_abr_brush_collection.h>
#include <kis_gbr_brush.h>
#include <kis_imagepipe_brush.h>
#include <KisGlobalResourcesInterface.h>

#include <cstdio>
#include <cstdlib>
#include <cstring>

#ifdef __AFL_HAVE_MANUAL_CONTROL
#  define FUZZ_INIT() __AFL_INIT()
#else
#  define FUZZ_INIT() do { } while (0)
#endif

enum Format { FMT_ABR, FMT_GBR, FMT_GIH };

static Format format_from_env()
{
    const char *f = getenv("KRITA_FUZZ_FORMAT");
    if (f && strcmp(f, "gbr") == 0) return FMT_GBR;
    if (f && strcmp(f, "gih") == 0) return FMT_GIH;
    return FMT_ABR;
}

int main(int argc, char **argv)
{
    // Krita's brush code builds QImages, so it needs a QGuiApplication. The
    // offscreen platform keeps that off any display.
    qputenv("QT_QPA_PLATFORM", "offscreen");
    qputenv("QT_LOGGING_RULES", "*=false");
    QGuiApplication app(argc, argv);

    const Format fmt = format_from_env();

    FUZZ_INIT();

    if (argc < 2) {
        fprintf(stderr, "usage: %s <file>   (KRITA_FUZZ_FORMAT=abr|gbr|gih)\n",
argv[0]);
        return 1;
    }

    QFile in(QString::fromLocal8Bit(argv[1]));
    if (!in.open(QIODevice::ReadOnly))
        return 0;
    QByteArray bytes = in.readAll();
    in.close();

    QBuffer dev(&bytes);
    if (!dev.open(QIODevice::ReadOnly))
        return 0;

    // The filename passed to the constructor is only a label Krita carries on
    // the resource; the bytes come from the device.
    switch (fmt) {
    case FMT_GBR: {
        KisGbrBrush brush(QStringLiteral("fuzz.gbr"));
        brush.loadFromDevice(&dev, KisGlobalResourcesInterface::instance());
        break;
    }
    case FMT_GIH: {
        KisImagePipeBrush brush(QStringLiteral("fuzz.gih"));
        brush.loadFromDevice(&dev, KisGlobalResourcesInterface::instance());
        break;
    }
    case FMT_ABR:
    default: {
        KisAbrBrushCollection coll(QStringLiteral("fuzz.abr"));
        coll.loadFromDevice(&dev);
        break;
    }
    }

    return 0;
}
```

2. Run it on the attached file:

```
KRITA_FUZZ_FORMAT=abr ./h_brush 07_abr_convertToQImage_line48.bin
```

Which reader runs is decided by the file the user opens; here it is chosen by
the variable so one binary serves all three, and `abr` is also the default.
Given the wrong reader the bytes are not parsed and the program exits 0.

3. It is deterministic: on the build with neither the address sanitizer nor
coverage instrumentation, the process terminated on a signal 5 runs out of 5
against 6.0.3. That confirmation was made at the time against 6.0.3; the build
no longer exists here, so there is no more recent re-run to report.

## more

The sampled-brush path reads the bounds and the depth straight out of the file
and derives the buffer size from them:

```cpp
454:        width = right - left;
455:        height = bottom - top;
456:        size = width * (depth >> 3) * height;
```

The only thing tested is an upper bound on the height:

```cpp
458:        /* FIXME: support wide brushes */
459:        if (height > 16384) {
```

`width` is not checked at all, neither dimension is required to be positive,
and nothing checks that the reads that produced them succeeded.

In this reproducer the file's last four bytes are `right`, so `depth` and
`compression` are read past the end. `QDataStream` leaves `depth` at 0, and
`getChar()` at line 452 leaves `compression` untouched, that is uninitialised.
The four bounds read are `top = 0`, `left = 0x00303030`, `bottom = 0x00003030`,
`right = 0x30303030`, so `width` is 805306368 and `height` is 12336 — which
clears the 16384 test — and `size` is `width * (0 >> 3) * height`, that is 0.

`convertToQImage()` then builds an image of those dimensions and walks it:

```cpp
41:    QImage img(width, height, QImage::Format_RGB32);
44:    for (int y = 0; y < height; y++) {
45:        QRgb *pixel = reinterpret_cast<QRgb *>(img.scanLine(y));
46:        for (int x = 0; x < width; x++, pos++) {
47:            value = 255 - buffer[pos];
48:            pixel[x] = qRgb(value, value , value);
```

One scanline of 805306368 RGB32 pixels is 3221225472 bytes, more than Qt
accepts for a scanline, so `img` is null. `scanLine()` returns a null pointer
for a null image, the loop bounds are the parameters rather than `img.width()`
and `img.height()` so the body runs anyway, and line 48 stores at address 0 —
the write to the zero page in the trace. The image is not tested with
`isNull()` after it comes back either: `brushTipImage.save()` at line 482 and
`setBrushTipImage()` at line 486 are reached with it.

Two guards cover this. `convertToQImage()` should not walk an image it failed
to create:

```diff
     QImage img(width, height, QImage::Format_RGB32);
+    if (img.isNull()) {
+        return img;
+    }
     int pos = 0;
```

and `abr_brush_load_v12()` should skip the brush whenever the header did not
parse into usable dimensions, rather than only when it is tall:

```diff
-        /* FIXME: support wide brushes */
-        if (height > 16384) {
-            warnKrita << "WARNING: wide brushes not supported";
+        if (abr.status() != QDataStream::Ok
+            || width <= 0 || height <= 0 || height > 16384) {
+            warnKrita << "WARNING: unusable brush dimensions, skipping";
             abr.device()->seek(next_brush);
         }
```

The `abr.status()` test also covers `compression`, which line 466 uses whether
or not line 452 managed to read it.

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

Reply via email to