xserver/test/pyxtest/proto/render.py
Octavia Togami bb59d6895f Fix use-after-free caused by duplicate glyphs in one glyphset
We make two glyphsets, GS1 and GS2, both with the same format depth.
GS1 contains any glyph different from the others, let's call it U
(unique). GS2 contains two glyphs with the same content, or at least
sha1 hash. Let's call them A and B.

We first register GS1 and GS2. GS1 has no problems, but before the
fix, registering GS2 breaks accounting in `globalGlyphs`. Since there
is no check for duplicate glyphs within a single set, we make two
`GlyphNew` entries for A and B. Now in the loop at the end, we go over
each entry and call `AddGlyph` and `FreeGlyph`:
1. AddGlyph(GS2, A, idA) inserts A into `globalGlyphs`
2. FreeGlyph(A) drops A's refcnt by one, but as it's being held in
   `globalGlyphs` it's kept alive.
3. AddGlyph(GS2, B, idB) finds A in `globalGlyphs` and reuses it
   instead of inserting B
4. FreeGlyph(B) drops B's refcnt by one (to zero), and B is freed.
   Before the fix, the lookup in `globalGlyphs` finds A's entry, and
   since it isn't a DeletedGlyph, it erroneously removes the
   `gr->glyph` pointing to `A` and decrements `tableEntries`. This
   results in `tableEntries` being off-by-one as `A` is still alive.

Now, we free the glyphsets in a particular order. Freeing GS1 results
in calling FreeGlyph(U), which will decrement U's refcnt to zero, and
free U. This clears out the entry in `globalGlyphs` for U and
decrements `tableEntries`, which due to the earlier error is now zero.
When `FreeGlyphSet` sees this, it clears the `globalGlyphs` for that
format depth. Now freeing A or GS2 (which transitively frees A) will
try to check the `globalGlyphs` at that format depth, but because it
was cleared out this crashes.

The fix here is to change FreeGlyph to only free a glyph from
`globalGlyphs` that is exactly the glyph being freed, so that
FreeGlyph(B) doesn't touch `globalGlyphs` at all. This keeps a proper
count in `tableEntries` and prevents `globalGlyphs[fdepth]` from being
freed before A is freed.

Closes: #1881
Part-of: <https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/2264>
(cherry picked from commit 81edc9caf557f7a44c5b308742814457de0eb6c0)
(cherry picked from commit 716d876142280cd66bdcf000f81428dadd5d3937)
Signed-off-by: Octavia Togami <octy@octyl.net>
2026-10-12 15:37:47 +02:00

354 lines
8.8 KiB
Python

# SPDX-License-Identifier: MIT
#
# Render extension protocol request builders
import struct
from dataclasses import dataclass, field
# Render minor opcodes
RenderQueryVersion = 0
RenderQueryPictFormats = 1
RenderCreatePicture = 4
RenderCreateGlyphSet = 17
RenderFreeGlyphSet = 19
RenderAddGlyphs = 20
RenderCompositeGlyphs8 = 23
RenderCompositeGlyphs16 = 24
RenderCompositeGlyphs32 = 25
RenderSetPictureFilter = 30
@dataclass
class QueryVersionRequest:
"""RenderQueryVersion request."""
opcode: int
major: int = 0
minor: int = 11
def to_bytes(self, byte_order: str = "<") -> bytes:
return struct.pack(
f"{byte_order}BBHII",
self.opcode,
RenderQueryVersion,
3,
self.major,
self.minor,
)
@dataclass
class QueryPictFormatsRequest:
"""RenderQueryPictFormats request."""
opcode: int
def to_bytes(self, byte_order: str = "<") -> bytes:
return struct.pack(
f"{byte_order}BBH",
self.opcode,
RenderQueryPictFormats,
1,
)
@dataclass
class PictFormInfo:
"""Parsed xPictFormInfo (28 bytes).
Layout:
id(4) type(1) depth(1) pad(2) direct(16) colormap(4)
direct is xDirectFormat:
red(2) redMask(2) green(2) greenMask(2)
blue(2) blueMask(2) alpha(2) alphaMask(2)
"""
id: int
type: int
depth: int
red: int
red_mask: int
green: int
green_mask: int
blue: int
blue_mask: int
alpha: int
alpha_mask: int
colormap: int
SIZE = 28
@classmethod
def from_bytes(
cls, data: bytes, offset: int, byte_order: str = "<"
) -> "PictFormInfo":
(
fid,
ftype,
fdepth,
_pad,
red,
red_mask,
green,
green_mask,
blue,
blue_mask,
alpha,
alpha_mask,
colormap,
) = struct.unpack_from(f"{byte_order}IBBH HHHH HHHH I", data, offset)
return cls(
id=fid,
type=ftype,
depth=fdepth,
red=red,
red_mask=red_mask,
green=green,
green_mask=green_mask,
blue=blue,
blue_mask=blue_mask,
alpha=alpha,
alpha_mask=alpha_mask,
colormap=colormap,
)
@dataclass
class QueryPictFormatsReply:
"""Parsed xRenderQueryPictFormatsReply.
Reply layout (32 bytes header):
type(1) pad(1) sequenceNumber(2) length(4)
numFormats(4) numScreens(4) numDepths(4) numVisuals(4)
numSubpixel(4) pad(4)
Followed by numFormats xPictFormInfo entries (28 bytes each).
"""
formats: list[PictFormInfo]
@classmethod
def from_reply(cls, data: bytes, byte_order: str = "<") -> "QueryPictFormatsReply":
"""Parse an X11Reply's raw data into a QueryPictFormatsReply."""
num_formats = struct.unpack_from(f"{byte_order}I", data, 8)[0]
formats = []
for i in range(num_formats):
off = 32 + i * PictFormInfo.SIZE
if off + PictFormInfo.SIZE > len(data):
break
formats.append(PictFormInfo.from_bytes(data, off, byte_order))
return cls(formats=formats)
@dataclass
class CreatePictureRequest:
"""RenderCreatePicture request."""
opcode: int
picture_id: int
drawable: int
format_id: int
value_mask: int = 0
def to_bytes(self, byte_order: str = "<") -> bytes:
return struct.pack(
f"{byte_order}BBHIIII",
self.opcode,
RenderCreatePicture,
5,
self.picture_id,
self.drawable,
self.format_id,
self.value_mask,
)
@dataclass
class CreateGlyphSetRequest:
"""RenderCreateGlyphSet request."""
opcode: int
glyph_set_id: int
format_id: int
def to_bytes(self, byte_order: str = "<") -> bytes:
return struct.pack(
f"{byte_order}BBHII",
self.opcode,
RenderCreateGlyphSet,
3,
self.glyph_set_id,
self.format_id,
)
@dataclass
class _CompositeGlyphsRequestBase:
"""Base class for RenderCompositeGlyphs{8,16,32} requests.
glyph_elts is raw xGlyphElt + glyph ID data.
Subclasses set _minor_opcode for the glyph size (8/16/32 bit IDs).
"""
_minor_opcode: int = field(default=0, init=False, repr=False)
opcode: int = 0
src_picture: int = 0
dst_picture: int = 0
glyph_set: int = 0
mask_format: int = 0
op: int = 3 # PictOpOver
src_x: int = 0
src_y: int = 0
glyph_elts: bytes = b""
def to_bytes(self, byte_order: str = "<") -> bytes:
total = 28 + len(self.glyph_elts)
pad_len = (4 - total % 4) % 4
length = (total + pad_len) // 4
header = struct.pack(
f"{byte_order}BBH BxH IIII hh",
self.opcode,
self._minor_opcode,
length,
self.op,
0, # pad2
self.src_picture,
self.dst_picture,
self.mask_format,
self.glyph_set,
self.src_x,
self.src_y,
)
return header + self.glyph_elts + b"\x00" * pad_len
@dataclass
class CompositeGlyphs8Request(_CompositeGlyphsRequestBase):
"""RenderCompositeGlyphs8 request (minor opcode 23)."""
_minor_opcode: int = field(default=RenderCompositeGlyphs8, init=False, repr=False)
@dataclass
class CompositeGlyphs16Request(_CompositeGlyphsRequestBase):
"""RenderCompositeGlyphs16 request (minor opcode 24)."""
_minor_opcode: int = field(default=RenderCompositeGlyphs16, init=False, repr=False)
@dataclass
class CompositeGlyphs32Request(_CompositeGlyphsRequestBase):
"""RenderCompositeGlyphs32 request (minor opcode 25)."""
_minor_opcode: int = field(default=RenderCompositeGlyphs32, init=False, repr=False)
@dataclass
class FreeGlyphSetRequest:
"""RenderFreeGlyphSet request."""
opcode: int
glyph_set_id: int
def to_bytes(self, byte_order: str = "<") -> bytes:
return struct.pack(
f"{byte_order}BBHI",
self.opcode,
RenderFreeGlyphSet,
2,
self.glyph_set_id,
)
@dataclass
class AddGlyphsRequest:
"""RenderAddGlyphs request.
Each glyph is described by a (glyph_id, glyph_info, data) tuple.
glyph_info is an xGlyphInfo: (width, height, x, y, x_off, y_off).
data is the raw pixel data for that glyph.
"""
opcode: int
glyph_set_id: int
glyphs: list[tuple[int, tuple[int, int, int, int, int, int], bytes]] = field(
default_factory=list
)
def to_bytes(self, byte_order: str = "<") -> bytes:
nglyphs = len(self.glyphs)
# glyph IDs (CARD32 each)
ids_data = b""
for gid, _, _ in self.glyphs:
ids_data += struct.pack(f"{byte_order}I", gid)
# xGlyphInfo entries (12 bytes each: HHhhhh)
infos_data = b""
for _, info, _ in self.glyphs:
infos_data += struct.pack(f"{byte_order}HHhhhh", *info)
# glyph pixel data (concatenated, each padded to 4-byte boundary)
pixel_data = b""
for _, _, data in self.glyphs:
pixel_data += data
pad = (4 - len(data) % 4) % 4
pixel_data += b"\x00" * pad
body = ids_data + infos_data + pixel_data
total = 12 + len(body)
pad_len = (4 - total % 4) % 4
length = (total + pad_len) // 4
header = struct.pack(
f"{byte_order}BBHII",
self.opcode,
RenderAddGlyphs,
length,
self.glyph_set_id,
nglyphs,
)
return header + body + b"\x00" * pad_len
@dataclass
class SetPictureFilterRequest:
"""RenderSetPictureFilter request.
The filter name is a variable-length string followed by padding,
then xFixed (CARD32) filter parameter values.
"""
opcode: int
picture: int
filter_name: str = "nearest"
params: list[int] | None = None # xFixed values (CARD32)
def to_bytes(self, byte_order: str = "<") -> bytes:
name_bytes = self.filter_name.encode("ascii")
name_len = len(name_bytes)
name_padded = name_bytes + b"\x00" * ((4 - name_len % 4) % 4)
param_data = b""
if self.params:
for p in self.params:
param_data += struct.pack(f"{byte_order}I", p)
total = 12 + len(name_padded) + len(param_data)
pad_len = (4 - total % 4) % 4
length = (total + pad_len) // 4
header = struct.pack(
f"{byte_order}BBH I H xx",
self.opcode,
RenderSetPictureFilter,
length,
self.picture,
name_len,
)
return header + name_padded + param_data + b"\x00" * pad_len