diff --git a/mk/config.mk b/mk/config.mk index 4525bfe..28fb62a 100644 --- a/mk/config.mk +++ b/mk/config.mk @@ -51,6 +51,11 @@ CPPFLAGS += -Iinclude -Isrc \ CFLAGS += -std=c99 -Wall -Wextra -Wno-unused-parameter \ -Wno-typedef-redefinition -fPIC +# Per-function/-data sections so the shared-library link can --gc-sections away +# code no exported symbol reaches once mk/library.mk pins the export surface. +# ELF-only leverage; clang on Mach-O ignores these and dead-strips by symbol. +CFLAGS += -ffunction-sections -fdata-sections + # Release optimization default for first-party objects: libX11-compat core, the # staged upstream libX11 sources, the compat toolkit libraries (libXt/Xpm/Xaw/ # Xmu/Xext/Xinerama/ICE/SM/Xft-compat), tests, and bundled examples. Debug CI diff --git a/mk/install.mk b/mk/install.mk index b0e1a2d..7ab9f87 100644 --- a/mk/install.mk +++ b/mk/install.mk @@ -11,6 +11,21 @@ PREFIX ?= /usr/local DESTDIR ?= +# Strip local symbols from the installed libraries: the export surface (dynamic +# symbols) is untouched, so only the internal names in .symtab go, shaving ~6% +# off each .so. The in-tree build/ copies keep their local symbols so crash +# backtraces stay symbolic during development; only the deployed artifact slims. +# Override STRIP (e.g. STRIP=: for a packager that strips its own way, or a +# cross strip) to change or disable it. +STRIP ?= strip + +# strip -x rewrites the Mach-O, which invalidates the ad-hoc signature ld64 puts +# on arm64 binaries. Apple's own strip re-signs, but lld or an older toolchain +# does not, and dyld then refuses the installed dylib ("code signature invalid"). +# Re-sign explicitly on Darwin so the default install is safe on any toolchain; +# elsewhere CODESIGN_RESIGN is the no-op colon builtin that just swallows the path. +CODESIGN_RESIGN := $(if $(filter Darwin,$(UNAME_S)),codesign --force --sign - ,:) + # Libraries a downstream links by their standard X11 SONAME (each gets a # libNAME.so -> libNAME-compat.so alias). XCOMPAT_INSTALL_ALIASED := X11 Xft Xext Xt Xmu Xaw Xpm Xinerama ICE SM @@ -34,10 +49,14 @@ install: $(XCOMPAT_INSTALL_LIB_FILES) $(UPSTREAM_HEADERS_STAMP) $(Q)mkdir -p "$(DESTDIR)$(PREFIX)/lib" "$(DESTDIR)$(PREFIX)/include" $(Q)for l in $(XCOMPAT_INSTALL_ALIASED); do \ cp "$(OUT)/lib$$l-compat.so" "$(DESTDIR)$(PREFIX)/lib/" && \ + $(STRIP) -x "$(DESTDIR)$(PREFIX)/lib/lib$$l-compat.so" && \ + $(CODESIGN_RESIGN) "$(DESTDIR)$(PREFIX)/lib/lib$$l-compat.so" && \ ln -sf "lib$$l-compat.so" "$(DESTDIR)$(PREFIX)/lib/lib$$l.so" || exit 1; \ done $(Q)for w in $(XCOMPAT_INSTALL_WRAPPERS); do \ - cp "$(OUT)/lib$$w.so" "$(DESTDIR)$(PREFIX)/lib/"; \ + cp "$(OUT)/lib$$w.so" "$(DESTDIR)$(PREFIX)/lib/" && \ + $(STRIP) -x "$(DESTDIR)$(PREFIX)/lib/lib$$w.so" && \ + $(CODESIGN_RESIGN) "$(DESTDIR)$(PREFIX)/lib/lib$$w.so" || exit 1; \ done $(Q)cp -R "$(UPSTREAM_HEADERS_DIR)/." "$(DESTDIR)$(PREFIX)/include/" $(Q)for d in $(XCOMPAT_INSTALL_HEADER_DIRS); do \ diff --git a/mk/library.mk b/mk/library.mk index f5d647a..59ca0cb 100644 --- a/mk/library.mk +++ b/mk/library.mk @@ -22,21 +22,98 @@ all: $(TARGET) # segfaults at offset 0x50. -Bsymbolic (not just -Bsymbolic-functions) # binds *both* function and data references to the local library at # link time. Mach-O ld on macOS does not accept the flag, so gate it -# on Linux only. -LDFLAGS_LIB := +# on Linux only. The version script below now hides those same lock +# globals outright (they are in no manifest, so local: keeps them out +# of the dynamic table entirely), which is strictly stronger than +# -Bsymbolic; the flag stays as belt-and-suspenders for the exported +# symbols' own intra-library references. + +# Pin the exported surface to the intended public API. The library defaults to +# exporting every non-static symbol, which leaks ~400 internal helpers into the +# dynamic table; restricting the export set to the enforced manifests also lets +# the link garbage-collect code no exported symbol reaches (e.g. the Xft/Fc copy +# that only libXft-compat consumes). See scripts/gen-export-list.sh. +X11_EXPORT_MANIFESTS := tests/api-symbols.txt tests/shim-symbols.txt \ + tests/private-symbols.txt tests/whitebox-symbols.txt + +# GLX (and the export FORMAT) toggle the exported surface, so a GLX=0->1 flip +# without make clean must not reuse a map that hid glX* as local. That identity +# is tracked once by X11_LINK_CONFIG below: the defined-syms, the export list, +# and the linked libraries all take it as a prerequisite, so a change to GLX or +# FORMAT regenerates the map and relinks. The artifact names stay unversioned. +# The core link splits in two: LDFLAGS_LIB_COMMON is the binding every core .so +# needs (notably -Bsymbolic on Linux), LDFLAGS_LIB_PIN is the export-surface +# restriction only the shipped library wants. The whitebox test twin below +# reuses COMMON but drops PIN, so it keeps -Bsymbolic (without which the system +# libX11.so.6 that SDL2 loads on Linux interposes our lock/event globals) while +# exporting every internal the tests reach. The @loader_path rpath and the +# @rpath install_name / $ORIGIN rpath come from shared_lib_rpath_ldflags, keyed +# on $@ so each library carries its own soname. +LDFLAGS_LIB_COMMON := +X11_RPATH_FLAGS = $(call shared_lib_rpath_ldflags,$(notdir $@)) ifeq ($(UNAME_S),Linux) - LDFLAGS_LIB += -Wl,-Bsymbolic $(call shared_lib_rpath_ldflags,$(notdir $(TARGET))) + X11_EXPORT_LIST := $(OUT)/libX11-compat.map + X11_EXPORT_FORMAT := elf + LDFLAGS_LIB_COMMON += -Wl,-Bsymbolic + LDFLAGS_LIB_PIN := -Wl,--version-script=$(X11_EXPORT_LIST) -Wl,--gc-sections endif ifeq ($(UNAME_S),Darwin) - # @loader_path lets the dylib find sibling compat shared libraries - # (libXt-compat, libXpm-compat, etc.) at the same directory level - # without requiring the consumer to bake in an absolute rpath. - LDFLAGS_LIB += -Wl,-install_name,@rpath/$(notdir $(TARGET)) \ - -Wl,-rpath,@loader_path + X11_EXPORT_LIST := $(OUT)/libX11-compat.exports + X11_EXPORT_FORMAT := macho + LDFLAGS_LIB_PIN := -Wl,-exported_symbols_list,$(X11_EXPORT_LIST) -Wl,-dead_strip endif -$(TARGET): $(OBJS) $(SDL_WRAPPER_TARGETS) | $(OUT) +X11_LINK_CONFIG := $(OUT)/libX11-compat.link-config +.PHONY: FORCE +$(X11_LINK_CONFIG): FORCE | $(OUT) + $(Q){ printf 'GLX=%s\n' '$(GLX)'; \ + printf 'FORMAT=%s\n' '$(X11_EXPORT_FORMAT)'; } > $@.tmp + $(Q)if test -r $@ && cmp -s $@.tmp $@; then \ + rm -f $@.tmp; \ + else \ + mv $@.tmp $@; \ + fi + +# The symbols the core objects actually define, so the export list can be +# intersected against them (see scripts/gen-export-list.sh for why). Darwin nm +# spells C symbols with a leading underscore; strip it so names match the +# manifests. Undefined entries (type U) are dropped. +X11_EXPORT_DEFINED := $(OUT)/libX11-compat.defined-syms +$(X11_EXPORT_DEFINED): $(OBJS) $(X11_LINK_CONFIG) | $(OUT) + @echo " GEN $@" + $(Q)nm -g $(OBJS) 2>/dev/null \ + | awk '$$1 ~ /^[0-9a-fA-F]+$$/ { print $$NF }' \ + | $(if $(filter Darwin,$(UNAME_S)),sed 's/^_//',cat) \ + | LC_ALL=C sort -u > $@ + $(Q)test -s $@ || { echo " ERROR $@ empty (nm found no defined symbols)" >&2; exit 1; } + +# X11_EXPORT_FORMAT and GLX are quoted because a make GLX= override leaves GLX +# empty; unquoted it would vanish from the argv and shift every later positional, +# so the script would read the defined-syms path as the glx flag. Matching only +# lines whose first nm field is a hex address above keeps undefined-weak (type +# w/v) references out of the defined set, not just type U. +$(X11_EXPORT_LIST): $(X11_EXPORT_MANIFESTS) $(X11_EXPORT_DEFINED) \ + scripts/gen-export-list.sh $(X11_LINK_CONFIG) | $(OUT) + @echo " GEN $@" + $(Q)scripts/gen-export-list.sh "$(X11_EXPORT_FORMAT)" "$(GLX)" \ + $(X11_EXPORT_DEFINED) $(X11_EXPORT_MANIFESTS) > $@ + +$(TARGET): $(OBJS) $(SDL_WRAPPER_TARGETS) $(X11_EXPORT_LIST) \ + $(X11_LINK_CONFIG) | $(OUT) + @echo " LD $@" + $(Q)$(CC) $(LDFLAGS) $(LDFLAGS_LIB_COMMON) $(X11_RPATH_FLAGS) \ + $(LDFLAGS_LIB_PIN) -shared -o $@ $(OBJS) $(LDLIBS) + +# Fat, unpinned twin of the core library for the whitebox tests (see mk/tests.mk +# for why they cannot link the pinned .so or the bare objects). Same objects and +# the same COMMON binding as $(TARGET), so -Bsymbolic still shields our globals +# from the system libX11.so.6, but with no export list every internal stays +# reachable. Built only as a prerequisite of the whitebox test binaries, never +# by all and never installed. +X11_TEST_LIB := $(OUT)/libX11-compat-test.so +$(X11_TEST_LIB): $(OBJS) $(SDL_WRAPPER_TARGETS) $(X11_LINK_CONFIG) | $(OUT) @echo " LD $@" - $(Q)$(CC) $(LDFLAGS) $(LDFLAGS_LIB) -shared -o $@ $(OBJS) $(LDLIBS) + $(Q)$(CC) $(LDFLAGS) $(LDFLAGS_LIB_COMMON) $(X11_RPATH_FLAGS) \ + -shared -o $@ $(OBJS) $(LDLIBS) endif # native .so build diff --git a/mk/tests.mk b/mk/tests.mk index baf097b..8f79b90 100644 --- a/mk/tests.mk +++ b/mk/tests.mk @@ -6,6 +6,7 @@ CHECK_BINS := $(OUT)/tests/check $(OUT)/tests/symbol-coverage \ $(OUT)/tests/test-xinerama-link \ $(OUT)/tests/test-libxpm-link \ $(OUT)/tests/test-xft-link \ + $(OUT)/tests/test-xlibint-link \ $(OUT)/tests/test-xtest # The GLX tests only exist when the optional GLX layer is built (GLX=1). # test-glx-link covers the no-provider degrade path; test-glx-provider drives the @@ -238,6 +239,25 @@ $(OUT)/tests/test-glx-init-fail: tests/test-glx-init-fail.c $(TARGET) $(FAKE_EGL $(Q)$(CC) $(CPPFLAGS) -DFAKE_EGL_PATH=\"$(abspath $(FAKE_EGL_LIB))\" \ $(FP_CFLAGS) $(CFLAGS_EXTRA) $< $(TARGET) $(LDLIBS) $(TEST_LDFLAGS) -o $@ +# check and test-xtest are whitebox tests: they call core internals directly. +# Linking them against the fat test twin (libX11-compat-test.so) instead of the +# pinned .so keeps those ~50 internals out of the shipped library's export list. +# The twin, not the bare objects, because on Linux the whitebox binary shares a +# process with the system libX11.so.6 that SDL2 loads; only a -Bsymbolic shared +# library (which an executable cannot be) keeps our lock/event globals from being +# interposed by it. Everything else links the pinned .so via the rule below. +# +# The twin only exists on the native .so build; under WASM=1 X11_TEST_LIB is +# empty, so fall back to $(TARGET), which is the full-symbol static archive +# there (no export pinning, no system libX11 to interpose) exactly as the +# generic rule linked these before this change. +X11_WHITEBOX_LIB := $(if $(X11_TEST_LIB),$(X11_TEST_LIB),$(TARGET)) +$(OUT)/tests/check $(OUT)/tests/test-xtest: $(OUT)/tests/%: tests/%.c $(X11_WHITEBOX_LIB) + @mkdir -p $(dir $@) + @echo " CC $<" + $(Q)$(CC) $(CPPFLAGS) $(FP_CFLAGS) $(CFLAGS_EXTRA) $< $(X11_WHITEBOX_LIB) \ + $(LDLIBS) $(TEST_LDFLAGS) -o $@ + $(OUT)/tests/%: tests/%.c $(TARGET) @mkdir -p $(dir $@) @echo " CC $<" diff --git a/mk/xcompat-libs.mk b/mk/xcompat-libs.mk index 6940802..cbe1741 100644 --- a/mk/xcompat-libs.mk +++ b/mk/xcompat-libs.mk @@ -107,9 +107,21 @@ $(SM_COMPAT_TARGET): $(OUT)/sm-compat.o $(ICE_COMPAT_TARGET) | $(OUT) $(Q)$(CC) $(LDFLAGS) $(SM_COMPAT_LDFLAGS) -shared -o $@ $< \ -L$(OUT) -lICE-compat +# -Wl,--no-undefined makes a missing core helper (one src/xft.c calls but that is +# absent from tests/private-symbols.txt) fail this link loudly on Linux, matching +# the macOS two-level namespace which already rejects it. Every symbol xft-compat.o +# references resolves from -lX11-compat + $(LDLIBS) (SDL, SDL_ttf, pixman, libc), +# so this only tightens error reporting, it does not change what links. macOS ld64 +# spells the same guard -Wl,-undefined,error, which is already its default. +# +# Dropped under a sanitizer build: -fsanitize leaves the __asan_*/__ubsan_* +# runtime symbols undefined in the .so (resolved from the executable at load +# time), which --no-undefined would reject, breaking the ASan/UBSan/TSan jobs. +XFT_SANITIZED := $(findstring -fsanitize,$(CFLAGS_EXTRA) $(LDFLAGS)) +XFT_COMPAT_NO_UNDEF := $(if $(filter Linux,$(UNAME_S)),$(if $(XFT_SANITIZED),,-Wl$(comma)--no-undefined)) $(XFT_COMPAT_TARGET): $(OUT)/xft-compat.o $(TARGET) | $(OUT) @echo " LD $@" - $(Q)$(CC) $(LDFLAGS) $(XFT_COMPAT_LDFLAGS) -shared -o $@ $< \ + $(Q)$(CC) $(LDFLAGS) $(XFT_COMPAT_LDFLAGS) $(XFT_COMPAT_NO_UNDEF) -shared -o $@ $< \ -L$(OUT) -lX11-compat $(LDLIBS) .PHONY: xext xmu xinerama ice sm xft diff --git a/scripts/gen-export-list.sh b/scripts/gen-export-list.sh new file mode 100755 index 0000000..e28f5ac --- /dev/null +++ b/scripts/gen-export-list.sh @@ -0,0 +1,77 @@ +#!/bin/sh + +# Emit the linker export list that pins libX11-compat's exported surface to its +# intended API. Everything else (internal helpers, the duplicated Xft/Fc copy) +# is hidden, which also lets the linker dead-strip code no exported symbol +# reaches. The symbol set is the union of the manifests already enforced by +# tests/check-api-symbols.py plus tests/private-symbols.txt (the core->libXft +# private contract) and tests/whitebox-symbols.txt (the whitebox test surface), +# so the manifests stay the single source of truth. +# +# The union is intersected with , the symbols the core objects +# actually define, so the list never names a symbol absent from this build. That +# matters because a manifest carries symbols that only exist on one platform or +# feature build (the libx11Compat* shims are macOS-only; glX* drop when GLX=0): +# a stray name is a hard error under ld64 and under lld's --no-undefined-version +# default, and a silent no-op only under GNU ld. Pinning to the defined set +# keeps every linker happy without per-platform manifests. +# +# Usage: gen-export-list.sh ... +set -eu + +# comm below needs both inputs collated identically to the sort that produced +# them; pin C collation everywhere so the intersection is deterministic +# regardless of the caller's LC_* (a UTF-8 locale orders _/case differently). +export LC_ALL=C + +format=$1 +glx=$2 +defined=$3 +shift 3 + +# Read every manifest in one checked step: a missing or unreadable file must +# abort the build, not silently yield an empty (API-omitting) map. A sed failure +# propagates through this assignment under set -e; the later grep -v / sort only +# exit non-zero on the harmless all-blank case. +manifest_lines=$(sed -e 's/#.*//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' "$@") +syms=$(printf '%s\n' "$manifest_lines" | grep -v '^$' | sort -u) + +# grep here reads a pipe, so it can only exit non-zero by matching nothing (a +# legal empty filter, e.g. GLX=0 against a glX-only set): tolerate that. +if [ "$glx" != "1" ]; then + syms=$(printf '%s\n' "$syms" | grep -v '^glX' || true) +fi + +# Keep only symbols this build actually defines. comm reads $defined, so a read +# error there is real and must fail the build; comm already returns 0 for an +# empty intersection, so no explicit fallback is needed for the legitimate empty +# case. +syms=$(printf '%s\n' "$syms" | comm -12 - "$defined") + +# Always export the libX11 underscore ABI the core defines (_Xdebug, +# _XrmInternalStringToQuark, ...). Real libX11 exports these _X* globals, and +# legacy clients link against them directly (violawww's libIMG references +# _Xdebug); the manifests only enumerate the X[A-Z]* public names, so without +# this the underscore ABI would be hidden and those clients fail to link. Drawn +# from $defined, so every entry is guaranteed present in this build. +abi=$(grep -E '^_X' "$defined" || true) +syms=$(printf '%s\n%s\n' "$syms" "$abi" | grep -v '^$' | sort -u) + +case "$format" in + macho) + + # Mach-O -exported_symbols_list: one C symbol per line, leading + # underscore. + printf '%s\n' "$syms" | grep -v '^$' | sed 's/^/_/' + ;; + elf) + # ELF version script: named globals, everything else local (hidden). + printf '{\n global:\n' + printf '%s\n' "$syms" | grep -v '^$' | sed 's/^/ /; s/$/;/' + printf ' local:\n *;\n};\n' + ;; + *) + echo "gen-export-list.sh: unknown format '$format'" >&2 + exit 2 + ;; +esac diff --git a/src/atoms.c b/src/atoms.c index 0f3e3f7..27e9136 100644 --- a/src/atoms.c +++ b/src/atoms.c @@ -17,6 +17,7 @@ static Bool predefinedAtomMatchesName(const char *predefinedName, { if (strcmp(predefinedName, name) == 0) return True; + /* The predefined list stores the C identifier (e.g. "XA_PRIMARY"); the X11 * atom name strips the "XA_" prefix. */ @@ -24,7 +25,7 @@ static Bool predefinedAtomMatchesName(const char *predefinedName, strcmp(&predefinedName[3], name) == 0; } -AtomStruct *getAtomStruct(Atom atom) +static AtomStruct *getAtomStruct(Atom atom) { AtomStruct *atomStruct = atomStorageStart; while (atomStruct) { @@ -35,7 +36,7 @@ AtomStruct *getAtomStruct(Atom atom) return NULL; } -AtomStruct *getAtomStructByName(const char *name) +static AtomStruct *getAtomStructByName(const char *name) { size_t i; for (i = 0; i < PREDEFINED_ATOM_LIST_SIZE; i++) { @@ -117,7 +118,9 @@ Status XGetAtomNames(Display *dpy, Atom *atoms, int count, char **names_return) return returned_names == count ? 1 : 0; } -Atom _internAtom(const char *atomName, Bool only_if_exists, Bool *outOfMemory) +static Atom _internAtom(const char *atomName, + Bool only_if_exists, + Bool *outOfMemory) { if (outOfMemory) *outOfMemory = False; diff --git a/src/colors.c b/src/colors.c index c096744..66ea268 100644 --- a/src/colors.c +++ b/src/colors.c @@ -71,17 +71,6 @@ SDL_Color uLongToColor(XcPixelFormat pixelFormat, unsigned long color) return res; } -SDL_Color uLongToColorFromVisual(Visual *visual, unsigned long color) -{ - SDL_Color res; - res.r = (visual->red_mask & color) >> 24; - res.g = (visual->green_mask & color) >> 16; - res.b = (visual->blue_mask & color) >> 8; - res.a = - (~(visual->red_mask | visual->green_mask | visual->blue_mask)) & color; - return res; -} - int XFreeColormap(Display *display, Colormap colormap) { // https://tronche.com/gui/x/xlib/color/XFreeColormap.html @@ -258,6 +247,7 @@ static Bool parseRgbComponent(const char **cursor, unsigned short *value) unsigned int raw = 0; for (int i = 0; i < digits; i++) raw = (raw << 4) | (unsigned int) hexValue(start[i]); + /* X11 rgb: components scale a k-digit value to 16 bits by bit replication, * so rgb:f/f/f is full intensity (0xffff), not 0xf000. */ @@ -351,6 +341,7 @@ int XFreeColors(Display *display, // https://tronche.com/gui/x/xlib/color/XFreeColors.html SET_X_SERVER_REQUEST(display, X_FreeColors); TYPE_CHECK(colormap, COLORMAP, display, 0); + /* Direct-color visual: pixels are not allocated entries, so nothing to * release. */ @@ -367,6 +358,7 @@ Status XAllocColor(Display *display, Colormap colormap, XColor *screen_in_out) TYPE_CHECK(colormap, COLORMAP, display, 0); if (!screen_in_out) return 0; + /* Spec: on success, .pixel is filled with the allocated index and the * .red/.green/.blue fields with the actually rendered values (which may * differ from the requested ones on indexed visuals). The compat layer's diff --git a/src/cursor.c b/src/cursor.c index 627bcca..3efe14e 100644 --- a/src/cursor.c +++ b/src/cursor.c @@ -15,6 +15,7 @@ typedef struct { SDL_Cursor *sdlCursor; int hotspot_x, hotspot_y; + /* When non-NULL, srcBits/maskBits are stride-aligned packed bit arrays (1 * bit per pixel) describing the original source and (optional) mask * pixmaps. XRecolorCursor uses them to rebuild the SDL color cursor with @@ -82,6 +83,7 @@ static unsigned char *packBitsFromImage(XImage *image) { if (!image || image->width <= 0 || image->height <= 0) return NULL; + /* width + 7 must not signed-overflow, and stride * height must fit in * size_t. Reject anything past those limits so the bits[py*stride + * (px>>3)] write below can never run past the end of the allocation. @@ -166,13 +168,13 @@ static Cursor allocateCursor(Display *display, return cursorId; } -Cursor createPixmapCursor(Display *display, - SDL_Texture *source, - SDL_Texture *mask, - _Xconst XColor *foreground_color, - _Xconst XColor *background_color, - unsigned int x, - unsigned int y) +static Cursor createPixmapCursor(Display *display, + SDL_Texture *source, + SDL_Texture *mask, + _Xconst XColor *foreground_color, + _Xconst XColor *background_color, + unsigned int x, + unsigned int y) { /* Custom cursor bitmaps are not yet rendered; fall back to the system arrow * so the call always succeeds with a usable cursor. @@ -196,6 +198,7 @@ Cursor XCreatePixmapCursor(Display *display, // https://tronche.com/gui/x/xlib/pixmap-and-cursor/XCreatePixmapCursor.html SET_X_SERVER_REQUEST(display, X_CreateCursor); TYPE_CHECK(source, PIXMAP, display, None); + /* Validate the mask up front so any error path runs before the srcImg * allocation below. */ @@ -283,6 +286,7 @@ Cursor XCreateGlyphCursor(Display *display, { // https://tronche.com/gui/x/xlib/pixmap-and-cursor/XCreateGlyphCursor.html SET_X_SERVER_REQUEST(display, X_CreateGlyphCursor); + /* Glyph rasterization is not implemented; fall back to the same arrow * placeholder as createPixmapCursor's NULL-source path. */ @@ -348,6 +352,7 @@ int XRecolorCursor(Display *display, SET_X_SERVER_REQUEST(display, X_RecolorCursor); TYPE_CHECK(cursor, CURSOR, display, 0); Cursor_ *c = GET_CURSOR(cursor); + /* Without the original bits (font / system / fallback cursors) the cursor * cannot be recolored; preserve the existing cursor and report success. */ @@ -360,6 +365,7 @@ int XRecolorCursor(Display *display, return 1; SDL_Cursor *previous = c->sdlCursor; c->sdlCursor = rebuilt; + /* Re-attach the new cursor everywhere the old one was active. SDL tracks * one "current" cursor, so a SetCursor is enough. */ diff --git a/src/gc.c b/src/gc.c index 48be749..e65ac55 100644 --- a/src/gc.c +++ b/src/gc.c @@ -67,6 +67,7 @@ GC XCreateGC(Display *display, graphicContextStruct->gid = contextId; SET_XID_TYPE(contextId, GRAPHICS_CONTEXT); SET_XID_VALUE(contextId, gc); + /* Initialize every field to safe defaults before any fallible allocation. * XFreeGC walks gc->clipRects via free() and gc->font via * compatFontReleaseForGC; if the dashes malloc below fails the cleanup path @@ -77,6 +78,7 @@ GC XCreateGC(Display *display, gc->numDashes = 0; gc->function = GXcopy; gc->planeMask = 0xFFFFFFFF; + /* Per X11 spec the defaults are pixel indices 0 (foreground) and 1 * (background). This shim treats pixel values as direct ARGB (see * src/colors.h), so default to opaque black/white instead. Otherwise @@ -130,11 +132,11 @@ GContext XGContextFromGC(GC gc) return gc->gid; } -Bool setDashes(Display *display, - GraphicContext *gc, - const char dashes[], - size_t numDashes, - Bool verifyValues) +static Bool setDashes(Display *display, + GraphicContext *gc, + const char dashes[], + size_t numDashes, + Bool verifyValues) { if (verifyValues) { size_t i; @@ -257,6 +259,7 @@ Status XGetGCValues(Display *display, { // https://tronche.com/gui/x/xlib/GC/XGetGCValues.html GraphicContext *graphicContext = GET_GC(gc); + /* A GC id can resolve to NULL (freed or stale, for example a GC retired * when the client closed and reopened its display). * diff --git a/src/image.c b/src/image.c index 7c3b580..3809829 100644 --- a/src/image.c +++ b/src/image.c @@ -426,7 +426,7 @@ XImage *XCreateImage(Display *display, return image; } -char *getImageDataPointer(XImage *image, unsigned int x, unsigned int y) +static char *getImageDataPointer(XImage *image, unsigned int x, unsigned int y) { char *pointer = image->data; pointer += image->bytes_per_line * y; @@ -675,7 +675,7 @@ XImage *XSubImage(XImage *image, return subImage; } -int destroyImage(XImage *image) +static int destroyImage(XImage *image) { // https://tronche.com/gui/x/xlib/utilities/XDestroyImage.html if (image->data) diff --git a/src/input-method.c b/src/input-method.c index 8c8b026..152d821 100644 --- a/src/input-method.c +++ b/src/input-method.c @@ -20,6 +20,7 @@ static char *currLocaleModifierList = defaultLocaleModifierList; static XIC focusedInputConnection = NULL; + /* The IC XSetICFocus is about to install, live only across its preedit-clear * callback. XDestroyIC nulls it if that callback destroys the incoming IC, so * XSetICFocus can bail instead of dereferencing freed memory. @@ -168,6 +169,7 @@ unsigned long inputMethodSetCurrentText(char *text) free(text); return 0; } + /* id 0 is reserved to mean "no commit" (a real KeyPress leaves subwindow * None == 0), so skip it on wraparound. A collision with a still-live id * would need ~2^64 commits with one never consumed, so it is not guarded. @@ -250,11 +252,13 @@ void inputMethodUnsetFocus(XIC inputConnection) return; if (focusedInputConnection) inputMethodHandlePreedit("", 0); + /* The clear above can fire a done callback that re-focuses another IC; do * not clobber that. Only finish the unset when this IC still holds focus. */ if (inputConnection && focusedInputConnection != inputConnection) return; + /* If that clear was a reentrant no-op (we were called from inside a preedit * callback), preeditActive may still be set; clear it so the IC is not * stuck mid-preedit and skipping its start callback when focused again. @@ -297,6 +301,7 @@ static void setInternalPreeditFont(_XIC *ic, GC gc) "*Arial Unicode*", "*Hiragino Sans GB*", "*STHeiti*", "*Noto Sans CJK*", "*Source Han Sans*", "*WenQuanYi*", "*Droid Sans Fallback*", "helvetica"}; + /* Resolve the preedit font once per IC and cache it. Probing up to eight * patterns on every keystroke while composing is needless work, and * XDestroyIC frees the cached font. @@ -327,6 +332,7 @@ static void drawInternalPreedit(_XIC *ic, const char *text, int len) clearInternalPreedit(ic); if (len <= 0) return; + /* Preedit strings are short; cap before len * 8 so a pathological length * cannot overflow the rect math or hand XDrawString a huge count. */ @@ -404,6 +410,7 @@ static void handlePreeditImpl(const char *text, int caret) { if (!focusedInputConnection) return; + /* User preedit callbacks below can re-enter Xlib and XDestroyIC or unfocus * this IC. Capture it so each callback can be followed by a check that * bails before touching freed or no-longer-focused state. @@ -413,6 +420,7 @@ static void handlePreeditImpl(const char *text, int caret) const char *s = text ? text : ""; int len = (int) strlen(s); int charLen = countUtf8Chars(s); + /* The host reports where the insertion point sits inside the composition; * clamp it into the preedit so a bogus backend value cannot drive the caret * past the text. A negative value means "unreported", so trail at the end @@ -431,6 +439,7 @@ static void handlePreeditImpl(const char *text, int caret) if (ic->hasPreeditStartCallback && ic->preeditStartCallback.callback) { ic->preeditStartCallback.callback( (XIM) self, ic->preeditStartCallback.client_data, NULL); + /* A callback that destroyed/unfocused this IC drops focus, so bail * before touching freed or no-longer-focused state. While merely * destroying (focus unchanged, IC still alive) keep going so the @@ -611,6 +620,7 @@ void XDestroyIC(XIC inputConnection) if (ic->destroying) return; ic->destroying = True; + /* If XSetICFocus is mid-switch into this IC, tell it the target died so it * does not install and dereference this freed connection. */ @@ -747,7 +757,8 @@ static Bool parseCommonICAttributes(XIC inputConnection, return True; } -Bool parsePreEditAttributes(XIC inputConnection, XVaNestedList attributes) +static Bool parsePreEditAttributes(XIC inputConnection, + XVaNestedList attributes) { return parseCommonICAttributes(inputConnection, attributes, True); } @@ -852,7 +863,7 @@ static Bool fillCommonICAttributes(XIC inputConnection, return True; } -Bool fillPreEditAttributes(XIC inputConnection, XVaNestedList returnArgs) +static Bool fillPreEditAttributes(XIC inputConnection, XVaNestedList returnArgs) { return fillCommonICAttributes(inputConnection, returnArgs, True); } @@ -896,6 +907,7 @@ static char *setICListValues(XIC inputConnection, if (IS_MAPPED_TOP_LEVEL_WINDOW(topLevel)) { SDL_Window *sdlWindow = GET_WINDOW_STRUCT(topLevel)->sdlWindow; SDL_RaiseWindow(sdlWindow); + /* The input rect and host text input belong to the IC that owns * focus; reconfiguring an unfocused IC must not retarget or * stop the focused one's IME. @@ -938,7 +950,9 @@ static char *setICListValues(XIC inputConnection, return NULL; } -char *setICValues(XIC inputConnection, va_list arguments, Bool allowSetReadOnly) +static char *setICValues(XIC inputConnection, + va_list arguments, + Bool allowSetReadOnly) { char *key = NULL; while ((key = va_arg(arguments, char *))) { @@ -967,6 +981,7 @@ char *setICValues(XIC inputConnection, va_list arguments, Bool allowSetReadOnly) if (IS_MAPPED_TOP_LEVEL_WINDOW(topLevel)) { SDL_Window *sdlWindow = GET_WINDOW_STRUCT(topLevel)->sdlWindow; SDL_RaiseWindow(sdlWindow); + /* Retarget SDL's IM window only for the IC that owns focus, so * reconfiguring an unfocused IC does not move or stop the * focused one's host text input. The restart keeps printable @@ -1058,6 +1073,7 @@ XIC XCreateIC(XIM inputMethod, ...) if ((key = setICValues(inputConnection, argumentList, True))) { LOG("setICValues failed in %s because of key %s!\n", __func__, key); va_end(argumentList); + /* setICValues may have allocated inputRect before failing; XDestroyIC * tolerates a partially built IC and frees it. */ @@ -1154,6 +1170,7 @@ void XSetICFocus(XIC inputConnection) // do beyond enabling SDL's host IME while this IC owns focus. if (!inputConnection) return; + /* Switching focus directly from another IC (no XUnsetICFocus in between) * must tear down the old IC's on-screen preedit; clear it while it is still * the focused connection. That clear can fire the old IC's preedit @@ -1290,6 +1307,7 @@ XFontSet XCreateFontSet(Display *display, char *pattern = firstFontSetPattern(base_font_name_list); if (!pattern) return NULL; + /* Try the alias first when one exists; if it fails (or there is no alias) * try the original caller-supplied pattern before falling back to "fixed". * Otherwise an alias miss silently downgrades a loadable user pattern to @@ -1356,6 +1374,7 @@ int Xutf8LookupString(XIC inputConnection, return 0; } LOG("InputMethod Event! text = '%s'.\n", pendingText); + /* Xutf8LookupString returns a byte count and an unterminated string, so * the NUL is excluded from the length, the buffer check, and the copy. * size_t throughout: narrowing strlen to int first would let a >INT_MAX @@ -1369,6 +1388,7 @@ int Xutf8LookupString(XIC inputConnection, *status_return = XBufferOverflow; return textLen > (size_t) INT_MAX ? INT_MAX : (int) textLen; } + /* A single ASCII byte maps to a keysym (XLookupBoth); a multi-byte or * multi-character commit is text only, so report XLookupChars with no * keysym rather than deriving one from a UTF-8 continuation byte. diff --git a/src/util.c b/src/util.c index 175cbad..f7d8bfe 100644 --- a/src/util.c +++ b/src/util.c @@ -178,7 +178,7 @@ void *removeArray(Array *a, size_t index, Bool preserveOrder) return element; } -Bool equalCmp(void *element, void *arg) +static Bool equalCmp(void *element, void *arg) { return element == arg; } diff --git a/src/visual.c b/src/visual.c index c4058c8..7ead334 100644 --- a/src/visual.c +++ b/src/visual.c @@ -9,6 +9,7 @@ static Visual *VISUAL_LIST = NULL; static size_t NUM_VISUALS = 0; static int VISUAL_DEPTH = 24; + /* The shared Visual table is screen-0 visuals; a per-screen variant would need * a separate entry per screen. Keeping this a constant avoids global mutation * racing with concurrent visual lookups. @@ -22,6 +23,7 @@ Bool initVisuals() LOG("Warn: Visual memory already allocated!\n"); return True; } + /* Defer NUM_VISUALS so a malloc failure leaves the two globals consistent. * freeVisuals() walks NUM_VISUALS entries of VISUAL_LIST, and would * otherwise dereference NULL during failure cleanup. @@ -84,7 +86,7 @@ VisualID XVisualIDFromVisual(Visual *visual) return visual->visualid; } -void fillVisualInfo(XVisualInfo *info, Visual *visual) +static void fillVisualInfo(XVisualInfo *info, Visual *visual) { info->visual = visual; info->visualid = visual->visualid; @@ -174,6 +176,7 @@ Status XMatchVisualInfo(Display *display, LOG("Visuals memory is not initialized in %s!\n", __func__); return 0; } + /* Only the default screen has visuals registered; for any other screen * report no match rather than returning a visual whose `screen` field would * silently be rewritten to 0 by fillVisualInfo. diff --git a/src/window.c b/src/window.c index d0fd76e..e5b2bd2 100644 --- a/src/window.c +++ b/src/window.c @@ -2044,17 +2044,6 @@ int XReparentWindow(Display *display, return reparentWindowImpl(display, window, parent, x, y); } -int indexInWindowList(Window *windowList, int numWindows, Window window) -{ - int i; - for (i = 0; i < numWindows; i++) { - if (*windowList == window) - return i; - windowList++; - } - return -1; -} - Bool XTranslateCoordinates(Display *display, Window sourceWindow, Window destinationWindow, diff --git a/tests/private-symbols.txt b/tests/private-symbols.txt new file mode 100644 index 0000000..a43ced0 --- /dev/null +++ b/tests/private-symbols.txt @@ -0,0 +1,25 @@ +# Private core symbols that are not part of the public Xlib surface but must +# stay exported because an installed private header or a sibling compat library +# exposes them as a link contract. src/xft.c is compiled twice: once into the +# core .so and once into the standalone libXft-compat.so. The standalone copy +# calls these core-internal helpers, so hiding them would break the libXft +# link. Keep this list in sync with the core helpers src/xft.c reaches for. A +# missing entry fails the libXft-compat link loudly on both platforms: macOS via +# the two-level namespace, Linux via the -Wl,--no-undefined added to that link +# (mk/xcompat-libs.mk). The libX11 underscore +# ABI (_Xdebug, _XGetHostname, _XDefaultError, ...) is deliberately NOT listed +# here: the export generator matches every core-defined _X* symbol by pattern, +# so that surface is covered without hand-listing (see scripts/gen-export-list.sh). +applyShapeMaskOverDrawnRect +captureShapeMaskBaseline +clearRendererClip +compatFontOpenFamilyFallback +compatFontOpenFamilyFallbackForChar +getGcClipIterationCount +getWindowRenderer +getXidStruct +markPixmapReadbackDirty +presentDrawableRectIfVisible +SCREEN_WINDOW +setGcClipForIteration +setRendererDrawableClip diff --git a/tests/test-xlibint-link.c b/tests/test-xlibint-link.c new file mode 100644 index 0000000..35bf5d3 --- /dev/null +++ b/tests/test-xlibint-link.c @@ -0,0 +1,21 @@ +/* Downstream link check for private ABI declared by installed Xlibint.h. */ +#include +#include + +int main(void) +{ + char hostname[256]; + int (*default_error)(Display *, XErrorEvent *) = _XDefaultError; + + if (!default_error) { + fprintf(stderr, "_XDefaultError did not link\n"); + return 1; + } + if (_XGetHostname(hostname, (int) sizeof(hostname)) < 0) { + fprintf(stderr, "_XGetHostname failed\n"); + return 1; + } + + printf("test_xlibint_link: ok\n"); + return 0; +} diff --git a/tests/whitebox-symbols.txt b/tests/whitebox-symbols.txt new file mode 100644 index 0000000..832bc54 --- /dev/null +++ b/tests/whitebox-symbols.txt @@ -0,0 +1,8 @@ +# Core-internal symbols that must stay exported for whitebox tests which link +# the core .so (not the objects). The pure whitebox tests (check, test-xtest) +# link the core objects directly, so their internals need no export; but the +# Motif-tier link tests (test-motif-link, test-toolkit-probe) reach the core +# only through libXm -> libX11-compat.so, so a handful of internals they probe +# have to remain visible in the shared library. Keep this list minimal; the +# linker fails those tests loudly if an entry goes missing. +compatSelfScalingToolkitLoaded