One xkb_compose_state served the whole session, so a dead key left half
typed in one window was completed by the first key in the next. Two IBus
contexts through the real processkey(), before this:
want length 0, got length 2; byte 0: want <end>, got 0xc3 (text)
want 0, got 1 (req.op)
Context A pressed dead_acute; context B pressed e, got é, and went down
the composed-text branch instead of sending a key. Only COMPOSING is
sticky -- xkbcommon starts over by itself after COMPOSED and CANCELLED,
which the table in compose_test already pins -- and nothing here ever
called xkb_compose_state_reset.
That static was also fed from three procs, two of them live at once, with
no lock, which xkbcommon forbids. Reaching it needs two focused windows,
so it cannot be made to fail on demand and has no test: it goes because
the sharing goes, not because anything guards it.
So a state per frontend, each starting over when the key comes from
another context. All are made in composeinit(), on threadmain, before
proccreate, because xkb_compose_state_new refs the table and that ref is
a plain increment --
b160: mov (%rdi),%edx b16a: add $0x1,%edx b170: mov %edx,(%rdi)
-- so making a state from a shared table on two procs would race in place
of the feed. Made before the procs exist they can still share the one
table. An owner address that gets reused says so with composedrop: ibus
hands out a contexts[] slot again, xim can malloc an Ic at a freed one,
and wl's single context, which serves every client in turn, drops at
every activate and deactivate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>