xserver/test/pyxtest
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Peter Hutterer bae088ccbd xkb: fix CheckKeySyms overwriting request-range symsPerKey entries
CheckKeySyms() has two loops: the first processes keys in the SetMap
request range, the second fills in any missing keys from the existing
map.

The indexing between the two was inconsistent: the first loop
uses a zero-based index + req->firstKeySym, the second loop used it just
as zero-based index.

Example:
   req->firstKeySym = 50
   req->nKeySyms = 10

  i goes from 0 to 10,
  loop 1 checks [50, 60), changes the width but finishes with i at 10.
  loop 2 then runs over [10, 255(, copying the old keymap width

The second loop overwrites the new widths with the width of the
existing keymap. The subsequent SetKeySyms worked on the new width
and may write fewer actions than the map width.

A subsequent XkbGetMap request would then read XkbKeyNumActions()
entries (derived from the new wider key_sym_map.width) from the shorter
action allocation, causing an out-of-bounds heap read. The overread
bytes would be copied into the GetMap reply and sent back to the client,
producing a bounded heap information disclosure.

This vulnerability was discovered by:
  WONJOON HWANG (@joon1337) working with TrendAI Zero Day Initiative

ZDI-CAN-32408

CVE-2026-93524

Assisted-by: Claude:claude-opus-4-6
Part-of: <https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/2341>
(cherry picked from commit cea71d0273)

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2026-10-20 19:54:36 +02:00
..
proto xkb: NULL text pointer after free in _CheckSetDoodad error path 2026-10-20 19:54:36 +02:00
asan.py
conftest.py test/pyxtest: keep parallel servers from taking over each other's display 2026-10-16 14:50:03 +02:00
ensure-meson-tests.sh
meson.build fb/mi/glamor: reject glyphs with negative dimensions 2026-07-14 11:54:22 +02:00
README.md ci: run pyxtest under valgrind memcheck on Ubuntu 2026-07-07 11:16:03 +02:00
test_fb.py test/pyxtest: fix ruff I001/UP035 import ordering 2026-08-20 14:33:53 +02:00
test_font.py Disable font server connections by default 2026-09-29 18:24:47 +02:00
test_glamor.py test/pyxtest: fix ruff I001/UP035 import ordering 2026-08-20 14:33:53 +02:00
test_glx.py glx: validate dataBytes against cmdlen in RenderLarge first request 2026-10-20 19:49:20 +02:00
test_present.py present: unlink notifies from window list in present_clear_window_notifies 2026-10-20 19:49:20 +02:00
test_randr.py test/pyxtest: move X11 error codes from xclient.py to proto/x11.py 2026-10-01 11:28:07 +02:00
test_record.py test/pyxtest: fix ruff I001/UP035 import ordering 2026-08-20 14:33:53 +02:00
test_render.py Fix use-after-free caused by duplicate glyphs in one glyphset 2026-10-12 15:37:47 +02:00
test_screensaver.py test/pyxtest: fix ruff I001/UP035 import ordering 2026-08-20 14:33:53 +02:00
test_shm.py test/pyxtest: fix ruff I001/UP035 import ordering 2026-08-20 14:33:53 +02:00
test_sync.py test/pyxtest: fix ruff I001/UP035 import ordering 2026-08-20 14:33:53 +02:00
test_vidmode.py test/pyxtest: fix ruff I001/UP035 import ordering 2026-08-20 14:33:53 +02:00
test_xi.py test/pyxtest: cover unbounded DEVICE_RESOLUTION SwapLongs 2026-10-01 17:17:14 +02:00
test_xinerama.py test/pyxtest: fix ruff I001/UP035 import ordering 2026-08-20 14:33:53 +02:00
test_xkb.py xkb: fix CheckKeySyms overwriting request-range symsPerKey entries 2026-10-20 19:54:36 +02:00
test_xres.py test/pyxtest: fix ruff I001/UP035 import ordering 2026-08-20 14:33:53 +02:00
valgrind.py
valgrind.suppressions
xclient.py test/pyxtest: keep parallel servers from taking over each other's display 2026-10-16 14:50:03 +02:00
xserver.py test/pyxtest: keep parallel servers from taking over each other's display 2026-10-16 14:50:03 +02:00

pyxtest - pytest-based X server test suite

This is a pytest-based test suite that launches X servers and sends crafted protocol requests to verify that security vulnerabilities and other bugs are properly handled.

It can be run against Xvfb, Xwayland, or Xorg but the latter typically requires root and/or some setup outside the test suite. The test suite uses both AddressSanitizer (ASAN) and valgrind for detecting memory errors such as out-of-bounds reads/writes and use-after-free.

Running tests

Via meson

The test suite (via Xvfb) is integrated into the meson tests and can be run with normal meson commands.

# run the python test suite
meson test --suite pyxtest
# run a set of tests
meson test pyxtest-test_randr.py

Consult the meson documentation for further details.

Directly with pytest

For running against a custom path, point the test suite at the server binary to test using environment variables or CLI options:

# Using environment variable
XVFB_PATH=build/hw/vfb/Xvfb pytest test/pyxtest/ -v

# Using --server-path
pytest test/pyxtest/ -v --server-path=build/hw/vfb/Xvfb

# Using the system Xvfb (fallback if no path is set)
pytest test/pyxtest/ -v

The normal pytest options work as expected (-k for test selection, etc.)

Tests can be run against a manually-started server using the --display option:

./build/hw/vfb/Xvfb :2
pytest test/pyxtest --display :2

Running with AddressSanitizer (ASAN)

ASAN is a compile-time instrumentation that detects memory errors such as heap buffer overflows and use-after-free. To use ASAN, build the server with sanitizer support:

meson setup build-asan -Db_sanitize=address -Db_lundef=false
meson compile -C build-asan

Then run the tests against the ASAN-built binary:

XSERVER_ASAN=1 XVFB_PATH=build-asan/hw/vfb/Xvfb pytest test/pyxtest/ -v

When using meson test, XSERVER_ASAN is set automatically if the build was configured with -Db_sanitize=address.

Tests marked with @pytest.mark.asan are skipped unless XSERVER_ASAN=1 is set. When ASAN detects an error, the server process is killed and the ASAN error report is included in the test failure message.

Note: ASAN and valgrind are mutually incompatible. When XSERVER_ASAN=1 is set, valgrind wrapping is automatically disabled even if --valgrind is passed.

Running with valgrind

The --valgrind flag runs all servers under valgrind:

pytest test/pyxtest/ -v --valgrind

Tests marked with @pytest.mark.valgrind automatically run their server under valgrind even without the --valgrind flag. This is useful for bugs that are only detectable via valgrind (e.g. use of uninitialised values).

Testing multiple server types

By default only Xvfb is tested. Use --server-type to test additional servers. Tests using the xserver fixture are automatically run once per server type:

pytest test/pyxtest/ -v --server-type=xvfb --server-type=xwayland

CLI options

Option Description
--valgrind Run all X servers under valgrind memcheck
--valgrind-suppressions=PATH Path to a valgrind suppressions file
--valgrind-log-dir=PATH Persist a per-test valgrind XML report here (plus a .txt summary when there are findings), independent of pass/fail. Also settable via VALGRIND_LOG_DIR.
--server-type=TYPE Server type to test (xvfb, xwayland, xorg). Repeatable. Default: xvfb
--server-path=PATH Explicit path to the X server binary
--display=DISPLAY Connect to an existing server instead of starting one. Accepts :N or N. Mutually exclusive with --valgrind and --server-path

Environment variables

The server binary is located by checking, in order:

  1. --server-path CLI option
  2. XVFB_PATH / XWAYLAND_PATH / XORG_PATH environment variable
  3. XSERVER_BUILDDIR environment variable (looks for hw/vfb/Xvfb etc.)
  4. build/ directory relative to the source root
  5. System PATH (prints a warning)

VALGRIND_SUPPRESSIONS can point to a suppressions file.

VALGRIND_LOG_DIR is the environment-variable form of --valgrind-log-dir.

XSERVER_ASAN set to 1 indicates the server binary was built with AddressSanitizer. This is set automatically by meson when -Db_sanitize=address is used. It can also be set manually.

Test markers

Marker Effect
@pytest.mark.asan Test requires ASAN (XSERVER_ASAN=1 must be set
@pytest.mark.valgrind Test requires valgrind (skipped if XSERVER_ASAN is set)
@pytest.mark.xwayland_only Test is skipped unless --server-type=xwayland
@pytest.mark.xorg_only Test is skipped unless --server-type=xorg
@pytest.mark.swapped_client Test uses a byte-swapped (big-endian) client connection

Writing a new test

  1. Create or edit a test_*.py file.

  2. Use the xserver and xclient or xclient_swapped fixtures to get a running server and connection:

    def test_something(self, xserver, xclient):
        # xclient is a RawX11Connection to xserver
        ...
        assert xserver.is_alive, "Server crashed"
    
  3. If the test needs an extension, create a fixture that handles negotiation:

    @pytest.fixture
    def render_client(xclient):
        ext = xclient.query_extension(Extension.RENDER)
        if not ext:
            pytest.skip("RENDER not available")
        # ... send version negotiation ...
        return xclient
    
  4. Build protocol requests using dataclasses from proto/ and send them with send_request(). The byte order is handled automatically based on the connection type (native or swapped):

    from proto import xi
    
    req = xi.XIChangeHierarchyRequest(
        opcode=opcode,
        num_changes=1,
        changes_data=change_data,
    )
    xclient.send_request(req)
    
  5. If a new extension module is needed, create proto/myext.py with constants and @dataclass request builders following the existing pattern.

  6. If the bug is only detectable via a memory sanitizer (OOB reads, use-after-free), mark the test with @pytest.mark.asan. Use @pytest.mark.valgrind only for bugs that specifically require valgrind (e.g. use of uninitialised values that ASAN does not detect).