Skip to content

Harden compat stack and hook violawww#7

Merged
jserv merged 2 commits into
mainfrom
violawww
Jun 8, 2026
Merged

Harden compat stack and hook violawww#7
jserv merged 2 commits into
mainfrom
violawww

Conversation

@jserv

@jserv jserv commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

This consolidates differential test that gates the compat stack against the ViolaWWW browser. Affected Xlib surface spans XCopyArea, XGrabPointer, XQueryPointer, XWarpPointer, XGeometry/XWMGeometry, XDrawString family, XListFonts, XPutImage, XDoubleToFixed, and the SDL event/grab pipeline.

Memory safety and contract:

  • xCopyArea raster-op fallback for non-GXcopy/masked planeMask via read-modify-write through SDL_RenderReadPixels; source read before the destination renderer is resolved (lazy GET_RENDERER can switch the active SDL target); calloc-backed pixel buffers; viewport-offset translation on the read rects
  • subtractSegment uses caller-provided scratch buffer and int64 extents; renderFillRectClipByChildren allocations bounded against SIZE_MAX wrap
  • font.c NULL string guards; decodeChar2bString rejects negative counts and caps allocation at (SIZE_MAX-1)/3; XDrawString16 double-free removed; XmbTextPerCharExtents rejects negative buffer size; XmbTextEscapement / Xutf8TextEscapement use int64 fallback; text extents clamp to short range
  • decodeString stripped to bounded copy + NUL-terminate; no more escape interpretation that mangled file paths
  • XListFonts returns caller-owned strdup'd entries with NULL sentinel; XFreeFontNames walks-and-frees; isSupportedFontFileName case-insensitive
  • XGeometry / XWMGeometry pixel math via new satMulAdd (__builtin_mul_overflow / __builtin_add_overflow) with clampInt64ToInt narrowing
  • XCopyArea int64 extent guards before SDL_Rect math
  • XDoubleToFixed clamps representable range; NaN returns 0
  • XPutImage stride validation (non-negative bytes_per_line, minimum row size for ZPixmap-32 and XYBitmap fast paths)

Pointer + grab contract:

  • XQueryPointer NULL-checks every output, type-checks window, reports pressed-button bits in mask_return
  • XWarpPointer containment and warp coord math in int64
  • XGrabPointer returns BadWindow/GrabNotViewable for invalid args, AlreadyGrabbed on second active grab, GrabInvalidTime via CARD32 signed-delta against SDL_GetTicks (tick-wrap safe)
  • XGrabButton replaces existing grab on (button, modifiers, window) tuple match instead of leaking duplicates
  • Grab cursor saved/restored on ungrab; activatePassiveButtonGrab uses new getContainingWindow helper

Concurrency:

  • putBackEventsLock, trackedDisplaysLock, activePointerWindowLock (the last is shared by activePointerWindow + pointerButtonState); pointerButtonStateSnapshot() helper for motion/wheel reads
  • putImageScratchLock with lazy SDL_AtomicLock spinlock init, process-long lifetime; lock/unlock helpers pass the acquired SDL_mutex handle so a lazy-init race cannot make a thread unlock what it did not lock
  • Event pipe fds initialized to -1, closed on last display close; dead fdopen(FILE*) leak path removed; closeEventPipe runs unconditional last-display teardown when remainingDisplays == 0

Build:

  • Debug-only NULL guards on GET_WINDOW_STRUCT, GET_GC, GET_DISPLAY via extension statement expressions (release builds unchanged)
  • STRICT=1 build variant adds -Werror to first-party objects only, applied via mk/common.mk and mk/xcompat-libs.mk
  • New differential umbrella target runs motif-demos-check and violawww as separate sub-makes with set +e; aggregate exit reports both statuses on failure (one regression cannot mask the other)
  • New mk/violawww.mk pinning the bolknote/violawww commit; Makefile includes it; externals/violawww/ added to .gitignore

CI:

  • ViolaWWW build step in the motif job after demo smoke checks
  • if: !cancelled() so a demo failure does not skip ViolaWWW (and ccache stats); workflow cancellation still aborts
  • ViolaWWW build log uploaded as artifact on failure

Build commands run: make, make check, make
CFLAGS_EXTRA=-DDEBUG_LIBX11_COMPAT check, make STRICT=1.


Summary by cubic

Adds in‑process XTest/replay with resize and snapshot support, and gates deterministic UI smoke tests for Motif and violawww across CI and local runs. Also embeds a 6x13 fixed‑bitmap font and tightens drawing, focus/grab, and event behavior for reliability.

  • New Features

    • XTest injection + scripted replay with motion/button/key, in‑process snapshots, and main‑thread resize; seqlock‑protected replay target for coherent (id, root) reads; tests/test-xtest added.
    • violawww gate: pinned build via mk/violawww.mk with patches to clear before redraw/scroll/resize and to use libXpm compat; new scroll/help replays, assertions, and differential runner.
    • Motif smoke: added ASan‑safe and iconv‑warning patches; runner uses a JSON assertion DSL; check-unit split from differential; CI installs DIFFERENTIAL_PKGS and runs smokes and references.
    • Fonts: embedded 6x13 bitmap fallback (used when LIBX11_COMPAT_FONT_DIR is unset) with length‑counted text paths; ASCII probe enables deterministic metrics.
    • API/behavior: XRenderSetPictureTransform stub; clamped XDoubleToFixed; top‑level windows start hidden and show on map; debug‑only NULL guards for GET_DISPLAY/GET_GC/GET_WINDOW_STRUCT.
  • Bug Fixes

    • Drawing/image: GX raster‑op read‑modify‑write fallback; XCopyArea bounds; XPutImage stride checks and process‑long scratch lock; default opaque alpha for core X11 pixels; present‑flush event on draw.
    • Focus/pointer/grabs: tri‑state focus with correct Notify detail; XGetInputFocus reports the real kind; RevertToParent walks past unviewable ancestors; destroy auto‑revert and queued‑event discard; strict grab semantics (active/passive), cursor save/restore, 64‑bit XWarpPointer, NULL‑safe XQueryPointer.
    • Concurrency/events: dedicated locks for event queues and XPutImage; event pipe fds init to ‑1 with ordered teardown; replay stops before pipe shutdown; snapshot/resize user events run on the main thread; seqlock root fallback if retries exhaust.
    • Build/CI/tests: STRICT=1 applies -Werror to first‑party only; separate Motif and violawww diff steps; thresholds tuned; Motif fileview smoke stabilized with corrected click coords and delay; README updated with violawww example.

Written for commit b8d0fe9. Summary will update on new commits.

Review in cubic

cubic-dev-ai[bot]

This comment was marked as resolved.

@jserv jserv force-pushed the violawww branch 2 times, most recently from 0e9bc42 to bb0984b Compare June 8, 2026 15:27
@sysprog21 sysprog21 deleted a comment from cubic-dev-ai Bot Jun 8, 2026
cubic-dev-ai[bot]

This comment was marked as resolved.

jserv added a commit that referenced this pull request Jun 8, 2026
…r guard

The CI gates surfaced one regression and a cubic-bot review (4451031162)
flagged three additional issues. All four are fixed here.

CI font-metric regression (build / debug-build / sanitize jobs):
- tests/check.c:5516 asserted that "*medium*-r-*--14*" returns the
  6x13 BDF metrics (ascent=12, descent=2, max_bounds.width=7), but
  fonts/6x13.bdf is intentionally not tracked in the repo and the
  loader returned False on CI runners, gating off the metric overrides
  in coreFontMetricsForName().
- scripts/embed-bdf-font.py decodes a BDF into a pre-populated
  unsigned char rows[CHAR_COUNT][HEIGHT] array; the generated
  src/font-6x13-bitmap.h is checked in as source code rather than as
  a data file, keeping the BDF asset itself out of the tree.
- src/font.c loadFixedBitmapFont() now falls back to the embedded
  table via useEmbeddedFixedBitmap() when LIBX11_COMPAT_FONT_DIR is
  unset or its 6x13.bdf is missing, so the fixed-bitmap path is
  always available and the metric overrides apply unconditionally.

Cubic review P3 / src/replay-target.h header guard:
- The previous guard _LIBX11_COMPAT_REPLAY_TARGET_H_ used the
  implementation-reserved underscore-uppercase prefix. Renamed to
  LIBX11_COMPAT_REPLAY_TARGET_H per C standard reservation rules.

Cubic review P3 / src/events.h RESIZE_EVENT_CODE comment:
- The doc claimed the payload was (data1<<16 | data2) packed
  dimensions, but the producer at snapshot.c:142-144 carries width
  in user.data1 and height in user.data2 as intptr_t. Comment now
  matches the actual wire format.

Cubic review P2 / src/replay-target.c retarget race:
- replayTargetWindowId() and replayTargetRootToLocal() each read a
  single atomic, so a caller using the two together could pick the
  id from one generation and the root from another if the main
  thread retargeted between the calls. The result: an XTest event
  routed to W1 with W2-relative local coords, mis-routing input.
- Added a seqlock counter (targetSeq) bumped to odd before the
  triple write and back to even after, in both replayTargetOffer
  Window and replayTargetForgetWindow.
- New replayTargetTranslateRoot() reads (id, rootX, rootY) under
  the seqlock with a small retry budget and returns a coherent
  snapshot plus translated local coordinates.
- replayTargetRememberPointer() also uses the seqlock when reading
  targetRootX/Y so a concurrent retarget cannot combine a new local
  origin with the previous target's root.
- src/xtest.c XTestFakeMotionEvent switches to the new snapshot API.
- src/wrapper/sdl-wrapper.c exposes SDL_AtomicAdd which the seqBump
  uses; previously the dlopen wrapper only forwarded SDL_AtomicGet/
  Set/CAS/Lock/Unlock.

Verified locally: make check returns 39 ok lines (exit 0).
jserv added a commit that referenced this pull request Jun 8, 2026
…ter fallback

Both gemini and codex flagged a HIGH-severity UAF window in
postPointerCrossingEvents: the function locked
activePointerWindowLock just long enough to snapshot oldHoverWindow
into a local variable, then released the lock and walked the
snapshotted XID via buildWindowPathToRoot. A concurrent XDestroyWindow
running between the unlock and the path walk passed
destroyWindow's clearPointerStateForWindow (lock briefly taken and
released) and freed the WindowStruct before the walking thread
dereferenced it. clearPointerStateForWindow alone could not close the
race because the racing thread already held a stale local copy.

- src/events.c postPointerCrossingEvents now holds the lock through
  buildWindowPathToRoot AND the appendPointerCrossingEvent calls, so
  a concurrent destroyWindow blocks at clearPointerStateForWindow
  until the walk completes. Lock-order verification: this function
  takes activePointer -> putBack (via appendPointer -> appendPutBack);
  destroyWindow takes putBack first via discardQueuedEventsForWindow
  and releases before reaching clearPointerStateForWindow, so the two
  threads never hold both locks simultaneously and there is no
  circular wait.
- queueNestedPointerLeaves was the same shape and gets the same
  treatment.

Codex also flagged a low-severity fallback bug in
replayTargetRememberPointer: if all 8 seqlock retries observed
either an odd seq or a sequence change, rx/ry stayed at 0, and the
next rebasePointerToTargetRoot() converted a local coordinate as if
it were root-relative.

- src/replay-target.c replayTargetRememberPointer now tracks
  gotSnapshot explicitly; on retry exhaustion it does a single best-
  effort read of the current root atomics instead of leaving (0, 0).

Gemini also noted that clearPointerStateForWindow omits the spec-
mandated LeaveNotify cascade per Xlib 10.5.5 when the hover window is
destroyed; that is a real spec gap but does not affect the CI gate or
introduce new races, so it is left for a follow-up.
@sysprog21 sysprog21 deleted a comment from cubic-dev-ai Bot Jun 8, 2026
Bring Xlib semantics to spec for the surfaces both clients hit:
- FocusKind tristate (None/PointerRoot/Window) in postFocusChange,
  matching Xlib 10.7.1 NotifyNonlinear/NotifyNonlinearVirtual on
  transitions that cross the root namespace.
- RevertToParent walks past unviewable ancestors via
  isWindowEffectivelyViewable; XGetInputFocus reports the actual
  tristate instead of always claiming PointerRoot for None.
- destroyWindow runs auto-revert and discards queued events before
  recursing into children, then clears pointer state to avoid UAF
  on a stale hover XID; postPointerCrossingEvents and
  queueNestedPointerLeaves hold the pointer lock through the full
  path walk and event emission.
- Length-counted text path threads size_t through the bitmap font
  fallback so XDrawString16 and the embedded 6x13 face render with
  the same length checks Xlib applies.
- BDF parser uses strtol with range check instead of atoi.

Add three self-contained subsystems libX11-compat needs to drive a
replay smoke harness from the host:
- src/replay.{c,h}: opcode parser for motion/button/key/wait
  sequences fed from LIBX11_COMPAT_REPLAY env.
- src/replay-target.{c,h}: seqlock around the (target window,
  root x, root y) triple so XTest readers see a coherent snapshot
  while top-level windows map and unmap.
- src/snapshot.{c,h}: PNG capture hook keyed off resize events.
- src/xtest.c + include/X11/extensions/XTest.h: XTest synthetic
  input wired through the replay target for deterministic clicks.

The embedded 6x13 fixed face (src/font-6x13-bitmap.h, generated
by scripts/embed-bdf-font.py) keeps the binary BDF out of the
repo while letting font.c fall back when SDL_ttf cannot locate a
matching face on the host.

Regression tests in tests/check.c, test-motif-link.c, and
test-xtest.c cover the focus event cascade, the destroy-while-
hovered race, the length-counted text path, and the replay-target
seqlock under retarget.
@sysprog21 sysprog21 deleted a comment from cubic-dev-ai Bot Jun 8, 2026
cubic-dev-ai[bot]

This comment was marked as resolved.

Build/CI infrastructure on top of the compat-layer changes:
- mk/motif.mk fetches and builds Motif against libX11-compat under
  $(OUT)/motif, runs the Motif demo binaries headless, and gates a Motif
  replay smoke (wsm-labels, fileview-done) under Xvfb.
- mk/violawww.mk builds ViolaWWW with the patches under
  compat/violawww-patches/ and gates a violawww replay smoke.
- mk/tests.mk introduces a check-unit / check-{smoke,differential}
  hierarchy: check-unit is the cheap, sanitizer-safe in-tree subset (no
  motif autoconf), and check rolls up unit + motif link/demos + smoke +
  differential.
- .github/workflows/ci.yml runs four jobs (build, debug-build, lint,
  sanitize) plus a motif job that exercises link, demos, smoke, and the
  two differential targets as independent steps. ccache and upstream-src
  caches use v2 key prefixes so the sanitize job no longer hits
  ASan-poisoned objects from the release-build slot.
- scripts/run-ui-replay.py drives the LIBX11_COMPAT_REPLAY engine under
  Xvfb, captures PNGs, and runs the JSON assertion DSL in
  tests/ui/assertions/; scripts/run-motif-differential-tests.py and
  scripts/run-violawww-differential-tests.py compare against a reference
  Xlib stack over SSH.
- compat/motif-patches/ adds two upstream tweaks (ASan-safe string and
  printer-list traversal, silencing iconv cascade warnings);
  compat/violawww-patches/ collects the eight patches ViolaWWW needs to
  render correctly against libX11-compat.
- tests/ui/ ships the replay scripts, assertion JSONs, and the scroll
  fixture HTML the violawww smoke serves.
@jserv jserv merged commit 62e3dbf into main Jun 8, 2026
5 checks passed
@jserv jserv deleted the violawww branch June 8, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant