mirror of
https://gitlab.freedesktop.org/xorg/xserver.git
synced 2026-06-06 22:18:24 +02:00
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>
This commit is contained in:
parent
6671daeada
commit
3568302483
3 changed files with 200 additions and 0 deletions
|
|
@ -55,6 +55,7 @@ if pytest.found()
|
|||
|
||||
# This needs to be kept in sync with the test_foo.py files in the tree
|
||||
tests_pyxtest = [
|
||||
'test_font.py',
|
||||
'test_glx.py',
|
||||
'test_present.py',
|
||||
'test_randr.py',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ from dataclasses import dataclass
|
|||
CreateWindow = 1
|
||||
CreatePixmap = 53
|
||||
InternAtom = 16
|
||||
ListFonts = 49
|
||||
SetFontPath = 51
|
||||
GetFontPath = 52
|
||||
QueryExtension = 98
|
||||
ChangeKeyboardMapping = 100
|
||||
ForceScreenSaverOpcode = 115
|
||||
|
|
@ -156,6 +159,110 @@ class ChangeKeyboardMappingRequest:
|
|||
return header + sym_data
|
||||
|
||||
|
||||
@dataclass
|
||||
class ListFontsRequest:
|
||||
"""X11 ListFonts request (opcode 49).
|
||||
|
||||
xListFontsReq (8 bytes header):
|
||||
reqType(1) + pad(1) + length(2) + maxNames(2) + nbytes(2)
|
||||
followed by the pattern string (padded to 4-byte boundary).
|
||||
"""
|
||||
|
||||
pattern: str
|
||||
max_names: int = 65535
|
||||
|
||||
def to_bytes(self, byte_order: str = "<") -> bytes:
|
||||
pat_bytes = self.pattern.encode("ascii")
|
||||
padded = _pad(pat_bytes)
|
||||
req_len = (8 + len(padded)) // 4
|
||||
header = struct.pack(
|
||||
f"{byte_order}BBH HH",
|
||||
ListFonts,
|
||||
0, # pad
|
||||
req_len,
|
||||
self.max_names,
|
||||
len(pat_bytes),
|
||||
)
|
||||
return header + padded
|
||||
|
||||
|
||||
@dataclass
|
||||
class SetFontPathRequest:
|
||||
"""X11 SetFontPath request (opcode 51).
|
||||
|
||||
xSetFontPathReq (8 bytes header):
|
||||
reqType(1) + pad(1) + length(2) + nFonts(2) + pad(2)
|
||||
followed by LISTofSTR8 (length-prefixed strings).
|
||||
"""
|
||||
|
||||
paths: list[str]
|
||||
|
||||
def to_bytes(self, byte_order: str = "<") -> bytes:
|
||||
# Build LISTofSTR8: each string is preceded by a 1-byte length
|
||||
payload = b""
|
||||
for p in self.paths:
|
||||
p_bytes = p.encode("ascii")
|
||||
payload += bytes([len(p_bytes)]) + p_bytes
|
||||
padded = _pad(payload)
|
||||
req_len = (8 + len(padded)) // 4
|
||||
header = struct.pack(
|
||||
f"{byte_order}BBH Hxx",
|
||||
SetFontPath,
|
||||
0, # pad
|
||||
req_len,
|
||||
len(self.paths),
|
||||
)
|
||||
return header + padded
|
||||
|
||||
|
||||
@dataclass
|
||||
class GetFontPathRequest:
|
||||
"""X11 GetFontPath request (opcode 52).
|
||||
|
||||
Simple 4-byte request with no parameters.
|
||||
"""
|
||||
|
||||
def to_bytes(self, byte_order: str = "<") -> bytes:
|
||||
return struct.pack(
|
||||
f"{byte_order}BBH",
|
||||
GetFontPath,
|
||||
0, # pad
|
||||
1, # length = 1 word
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GetFontPathReply:
|
||||
"""Parsed xGetFontPathReply.
|
||||
|
||||
xGetFontPathReply layout:
|
||||
type(1) + pad(1) + sequenceNumber(2) + length(4)
|
||||
nPaths(2) + pad(22)
|
||||
LISTofSTR8 (each entry: length-byte + string)
|
||||
"""
|
||||
|
||||
paths: list[str]
|
||||
|
||||
@classmethod
|
||||
def from_reply(cls, data: bytes) -> "GetFontPathReply":
|
||||
"""Parse an X11Reply's raw data into a GetFontPathReply."""
|
||||
paths: list[str] = []
|
||||
if len(data) < 32:
|
||||
return cls(paths)
|
||||
n_paths = struct.unpack_from("<H", data, 8)[0]
|
||||
offset = 32
|
||||
for _ in range(n_paths):
|
||||
if offset >= len(data):
|
||||
break
|
||||
slen = data[offset]
|
||||
offset += 1
|
||||
if offset + slen > len(data):
|
||||
break
|
||||
paths.append(data[offset : offset + slen].decode("ascii", errors="replace"))
|
||||
offset += slen
|
||||
return cls(paths)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ForceScreenSaver:
|
||||
"""X11 ForceScreenSaver request."""
|
||||
|
|
|
|||
92
test/pyxtest/test_font.py
Normal file
92
test/pyxtest/test_font.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# 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)
|
||||
Loading…
Add table
Reference in a new issue