Package: wv
Version: 1.2.9-9
Severity: important
Tags: security, upstream

Hello,

I am reporting a reproducible security issue in Debian wv 1.2.9-9 and upstream 
wvWare.

`wvGetBKD_PLCF()` in `bkd.c:73` frees the address of its `U32 **pos` parameter 
instead of the heap buffer it points to:

    wvFree(pos); /* incorrect */
    wvFree(*pos); /* intended */

A crafted Word 97 document with `lcbPlcftxbxBkd=4` makes `*nobkd=0`, so 
`wvMalloc(0)` returns NULL and the error path calls `free()` on a stack 
address. No real memory-pressure condition is needed.

Reproduction:

    wvConvert simple-bkd-len4.doc
    wvRTF simple-bkd-len4.doc

Both terminate with:

    free(): invalid pointer

ASan reports an attempted free of an address located in the `wvDecodeSimple` 
stack frame at `generic.c:67` / `bkd.c:73`. The same input was also tested with 
Ubuntu's prebuilt wv 1.2.9-4.2build1 binaries.

The same copy-paste error remains in nine other PLCF parsers: `ffn.c:253`, 
`lst.c:163`, `pcd.c:96`, `fld.c:298`, `atrd.c:69`, `bte.c:88`, `frd.c:63`, 
`ftxbxs.c:81`, and `fdoa.c:65`. I verified the BKD path end to end; these nine 
sites are reported as the same source-level pattern, not as nine separate CVE 
requests.

The current Debian 1.2.9-9 source still contains `wvFree(pos)` at `bkd.c:73`. 
The attached patch replaces the erroneous operand with `wvFree(*pos)`.

This is distinct from CVE-2026-112055 (FKP stack out-of-bounds read), 
CVE-2026-112056 (stylesheet heap out-of-bounds read), and Debian Bug #1148725 
(pap.c list-index issue). Confirmed impact is process termination; the document 
does not control an arbitrary free address or code execution.

Please assign a CVE if Debian considers this a vulnerability in the Debian 
package. I am happy to retest a patched build.

Thanks,
netwind
[email protected]

# wvWare invalid free in wvGetBKD_PLCF

## Summary

`wvGetBKD_PLCF()` frees the address of its `U32 **pos` parameter instead of the
heap buffer it points to. On the allocation-failure path the code executes
`wvFree(pos)` rather than `wvFree(*pos)`, so a malformed Word document that makes
the second allocation return NULL makes wvWare call `free()` on a stack address.

The same copy-paste mistake appears in several other PLCF parsers in the
library.

## Affected

- wvWare 1.2.9 (current release)
- `wvWare`, `wvConvert`, `wvRTF` and other utilities linked against libwv

## Reproduction

```
wvConvert simple-bkd-len4.doc
wvRTF     simple-bkd-len4.doc
```

Both abort with:

```
free(): invalid pointer
```

The crash also reproduces on the prebuilt `wvConvert` and `wvRTF` binaries from
the Ubuntu/Debian `wv` package (`wv_1.2.9-4.2build1`), so it is not an artifact
of the local build.

Under ASan:

```
ERROR: AddressSanitizer: attempting free on address which was not malloc()-ed
    #1 _wvFree generic.c:67
    #2 wvGetBKD_PLCF bkd.c:73
```

The PoCs are normal Word 97 documents with one FIB field changed:
`lcbPlcftxbxBkd` is set to 4 (or 13). With that length the parser computes
`*nobkd = 0`, so `wvMalloc(0 * sizeof(BKD))` returns NULL and the error path
runs.

## Root cause

`bkd.c:67-75`:

```c
*bkd = (BKD *) wvMalloc (*nobkd * sizeof (BKD));
if (*bkd == NULL)
  {
      ...
      wvFree (pos);   /* <-- frees the parameter, not *pos */
      return (1);
  }
```

`wvMalloc(0)` returns NULL by design, so any length that yields zero entries
reaches this path.

## Same pattern elsewhere

The identical `wvFree(pos)` mistake exists in these parsers:

- `ffn.c:253`
- `lst.c:163`
- `pcd.c:96`
- `fld.c:298`
- `atrd.c:69`
- `bte.c:88`
- `frd.c:63`
- `ftxbxs.c:81`
- `fdoa.c:65`

Each is reachable from a Word document by setting the matching `lcb*` field to
a value that makes the second allocation zero-sized.

## Suggested fix

Replace every `wvFree(pos)` in those error paths with `wvFree(*pos)`, and
preferably also zero the caller's pointer after freeing.

## Attachments

- `simple-bkd-len4.doc`
- `simple-bkd-len13.doc`

Attachment: wv-invalid-free.patch
Description: wv-invalid-free.patch

<<attachment: simple-bkd-len4.doc>>

<<attachment: simple-bkd-len13.doc>>

Attachment: wv-fix-verify.log
Description: wv-fix-verify.log

/* wvWare
 * Copyright (C) Caolan McNamara, Dom Lachowicz, and others
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <stdlib.h>
#include <stdio.h>
#include "wv.h"

void
wvGetBKD (BKD * item, wvStream * fd)
{
    U16 temp16;
    item->ipgd_itxbxs = (S16) read_16ubit (fd);
    item->dcpDepend = (S16) read_16ubit (fd);
    temp16 = read_16ubit (fd);
    item->icol = temp16 & 0x00FF;
    item->fTableBreak = (temp16 & 0x0100) >> 8;
    item->fColumnBreak = (temp16 & 0x0200) >> 9;
    item->fMarked = (temp16 & 0x0400) >> 10;
    item->fUnk = (temp16 & 0x0800) >> 11;
    item->fTextOverflow = (temp16 & 0x1000) >> 12;
    item->reserved1 = (temp16 & 0xE000) >> 13;
}

int
wvGetBKD_PLCF (BKD ** bkd, U32 ** pos, U32 * nobkd, U32 offset, U32 len,
               wvStream * fd)
{
    U32 i;
    if (len == 0)
      {
          *bkd = NULL;
          *pos = NULL;
          *nobkd = 0;
      }
    else
      {
          *nobkd = (len - 4) / (cbBKD + 4);
          *pos = (U32 *) wvMalloc ((*nobkd + 1) * sizeof (U32));
          if (*pos == NULL)
            {
                wvError (
                         ("NO MEM 1, failed to alloc %d bytes\n",
                          (*nobkd + 1) * sizeof (U32)));
                return (1);
            }

          *bkd = (BKD *) wvMalloc (*nobkd * sizeof (BKD));
          if (*bkd == NULL)
            {
                wvError (
                         ("NO MEM 1, failed to alloc %d bytes\n",
                          *nobkd * sizeof (BKD)));
                wvFree (pos);
                return (1);
            }
          wvStream_goto (fd, offset);
          for (i = 0; i < *nobkd + 1; i++)
            {
                (*pos)[i] = read_32ubit (fd);
            }
          for (i = 0; i < *nobkd; i++)
            {
                wvGetBKD (&((*bkd)[i]), fd);
            }
      }
    return (0);
}

Reply via email to