xserver/test/pyxtest/test_font.py
Peter Hutterer 3568302483 test/pyxtest: add test for font alias stack overflow (ZDI-CAN-30136)
Add ListFonts, SetFontPath, and GetFontPath protocol builders to
proto/x11.py and a regression test that reproduces the
doListFontsAndAliases stack buffer overflow.

The test creates a temporary font directory with a fonts.alias file
containing an alias whose target name is 400 bytes -- exceeding the
old XLFDMAXFONTNAMELEN of 256 but under libXfont2's MAXFONTNAMELEN of
1024. It prepends this directory to the font path via SetFontPath, then
sends ListFonts matching the alias name. Without the fix, the server
would copy the oversized resolved name into a 256-byte stack buffer,
causing a stack buffer overflow.

ZDI-CAN-30136

Assisted-by: Claude:claude-opus-4-6
Part-of: <https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/2228>
2026-06-02 09:41:02 +10:00

92 lines
3.7 KiB
Python

# SPDX-License-Identifier: MIT
#
# Security tests for font alias handling vulnerabilities.
import os
import time
import pytest
from proto import x11
from xclient import X11Reply
class TestFontAliasOverflow:
"""Tests for doListFontsAndAliases stack buffer overflow via long alias."""
@pytest.mark.asan
def test_list_fonts_long_alias_overflow(self, xserver, xclient, tmp_path):
"""
ZDI-CAN-30136: doListFontsAndAliases copies the resolved alias
target from libXfont2 into tmp_pattern[] and c->current.pattern[],
both sized XLFDMAXFONTNAMELEN. The server defined
XLFDMAXFONTNAMELEN as 256, but libXfont2 allows alias targets up
to MAXFONTNAMELEN (1024) bytes in fonts.alias files. A
fonts.alias with a target name between 257 and 1023 bytes caused
a stack buffer overflow when the alias was resolved via
ListFonts.
Attack:
1. Create a font directory with fonts.alias containing an alias
whose target name exceeds the old 256-byte buffer (but stays
under 1024 to pass libXfont2 validation).
2. SetFontPath to include this directory.
3. ListFonts with a pattern matching the alias name.
4. Server copies oversized resolved name into the undersized
stack and struct buffers -- stack buffer overflow.
Fixed by increasing XLFDMAXFONTNAMELEN to 1024 to match
libXfont2's MAXFONTNAMELEN.
"""
# The old XLFDMAXFONTNAMELEN was 256, now 1024
# MAXFONTNAMELEN in libXfont2 is 1024
# Use a target length > 256 but < 1024 to trigger the old bug.
# The overflow must be large enough to clobber the saved return
# address on the stack; 256 + 400 = 656 bytes overflows 400
# bytes past the tmp_pattern[256] buffer which reliably reaches
# the saved RIP and crashes the server.
target_len = 656
alias_name = "pwn"
# Step 1: Create evil font directory with long alias target
evil_dir = str(tmp_path / "evilfonts")
os.makedirs(evil_dir)
# fonts.dir (empty -- 0 fonts, required for FPE init)
with open(os.path.join(evil_dir, "fonts.dir"), "w") as f:
f.write("0\n")
# fonts.alias with oversized target name
# Use XLFD-like format starting with '-' so the FPE recognizes it
long_target = "-" + "A" * (target_len - 1)
with open(os.path.join(evil_dir, "fonts.alias"), "w") as f:
f.write(f"{alias_name} {long_target}\n")
# Step 2: Get current font path so we can restore it later
req = x11.GetFontPathRequest()
xclient.send_request(req.to_bytes())
resp = xclient.recv_response(timeout=5.0)
assert isinstance(resp, X11Reply), "GetFontPath failed"
original_paths = x11.GetFontPathReply.from_reply(resp.data).paths
# Step 3: Set font path to include evil directory first
new_paths = [evil_dir] + original_paths
req = x11.SetFontPathRequest(paths=new_paths)
xclient.send_request(req.to_bytes())
xclient.flush_responses(timeout=1.0)
# Step 4: ListFonts with pattern matching the alias name.
# This triggers doListFontsAndAliases which resolves the alias
# and copies the oversized target into the stack buffer.
req = x11.ListFontsRequest(pattern=alias_name, max_names=10)
xclient.send_request(req.to_bytes())
time.sleep(0.5)
assert xserver.is_alive, (
"Server crashed - font alias stack buffer overflow (ZDI-CAN-30136)"
)
# Step 5: Restore original font path
req = x11.SetFontPathRequest(paths=original_paths)
xclient.send_request(req.to_bytes())
xclient.flush_responses(timeout=1.0)