compose: a sequence belongs to one context

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>
This commit is contained in:
2026-08-17 23:25:22 +09:00
parent e64e4ea6af
commit 650f00812d
7 changed files with 84 additions and 24 deletions

View File

@@ -5,7 +5,13 @@
#include <xkbcommon/xkbcommon.h> #include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-compose.h> #include <xkbcommon/xkbcommon-compose.h>
static struct xkb_compose_state *cstate; /*
* One state per frontend, all made before the procs are: xkbcommon locks
* nothing, and even its table refcount is a plain increment. cowner is
* the context whose sequence a state holds.
*/
static struct xkb_compose_state *cstate[Ncompose];
static void *cowner[Ncompose];
/* Dead keys and Compose: no frontend's client does them for us. */ /* Dead keys and Compose: no frontend's client does them for us. */
void void
@@ -14,6 +20,7 @@ composeinit(void)
struct xkb_context *ctx; struct xkb_context *ctx;
struct xkb_compose_table *table; struct xkb_compose_table *table;
char *locale; char *locale;
int i;
locale = setlocale(LC_CTYPE, ""); locale = setlocale(LC_CTYPE, "");
ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
@@ -22,30 +29,51 @@ composeinit(void)
table = xkb_compose_table_new_from_locale(ctx, table = xkb_compose_table_new_from_locale(ctx,
locale != nil ? locale : "C", XKB_COMPOSE_COMPILE_NO_FLAGS); locale != nil ? locale : "C", XKB_COMPOSE_COMPILE_NO_FLAGS);
if(table != nil) if(table != nil)
cstate = xkb_compose_state_new(table, XKB_COMPOSE_STATE_NO_FLAGS); for(i = 0; i < Ncompose; i++)
cstate[i] = xkb_compose_state_new(table,
XKB_COMPOSE_STATE_NO_FLAGS);
xkb_compose_table_unref(table); xkb_compose_table_unref(table);
xkb_context_unref(ctx); xkb_context_unref(ctx);
} }
/* The context is going, and the sequence it left half typed with it. */
void
composedrop(int who, void *owner)
{
if(cstate[who] == nil || owner != cowner[who])
return;
xkb_compose_state_reset(cstate[who]);
cowner[who] = nil;
}
/* /*
* Feeds sym to the Compose table. Returns 1 while a sequence is * Feeds sym to the Compose table. Returns 1 while a sequence is
* unfinished: the key belongs to the sequence, not to the engine. A * unfinished: the key belongs to the sequence, not to the engine. A
* finished sequence leaves its text in buf, which the caller commits * finished sequence leaves its text in buf, which the caller commits
* after whatever the engine had pending. * after whatever the engine had pending. A sequence belongs to the
* context that started it: a key from another one starts over.
*/ */
int int
composekey(u32int sym, char *buf, int n) composekey(int who, void *owner, u32int sym, char *buf, int n)
{ {
struct xkb_compose_state *cs;
buf[0] = '\0'; buf[0] = '\0';
if(cstate == nil || cs = cstate[who];
xkb_compose_state_feed(cstate, sym) != XKB_COMPOSE_FEED_ACCEPTED) if(cs == nil)
return 0; return 0;
switch(xkb_compose_state_get_status(cstate)){ if(owner != cowner[who]){
xkb_compose_state_reset(cs);
cowner[who] = owner;
}
if(xkb_compose_state_feed(cs, sym) != XKB_COMPOSE_FEED_ACCEPTED)
return 0;
switch(xkb_compose_state_get_status(cs)){
case XKB_COMPOSE_COMPOSING: case XKB_COMPOSE_COMPOSING:
case XKB_COMPOSE_CANCELLED: case XKB_COMPOSE_CANCELLED:
return 1; return 1;
case XKB_COMPOSE_COMPOSED: case XKB_COMPOSE_COMPOSED:
xkb_compose_state_get_utf8(cstate, buf, n); xkb_compose_state_get_utf8(cs, buf, n);
break; break;
default: default:
break; break;

9
dat.h
View File

@@ -41,6 +41,15 @@ enum
Colselfg = 0xffffff, Colselfg = 0xffffff,
}; };
/* Each frontend composes on its own: they run in different procs. */
enum
{
Composeibus,
Composexim,
Composewl,
Ncompose,
};
/* Popup metrics in pixels; popupscale is GDK_SCALE, for HiDPI. */ /* Popup metrics in pixels; popupscale is GDK_SCALE, for HiDPI. */
extern int popupscale; extern int popupscale;
#define Fontsz (32*popupscale) #define Fontsz (32*popupscale)

3
fn.h
View File

@@ -48,7 +48,8 @@ void wlthread(void*);
int keymeaningful(u32int); int keymeaningful(u32int);
void composeinit(void); void composeinit(void);
int composekey(u32int, char*, int); void composedrop(int, void*);
int composekey(int, void*, u32int, char*, int);
void* emalloc(ulong); void* emalloc(ulong);
void* erealloc(void*, ulong); void* erealloc(void*, ulong);

3
ibus.c
View File

@@ -509,6 +509,7 @@ static void
dropcontext(Ictx *ctx) dropcontext(Ictx *ctx)
{ {
releasecontext(ctx); releasecontext(ctx);
composedrop(Composeibus, ctx);
if(preowner == ctx) if(preowner == ctx)
preowner = nil; preowner = nil;
memset(ctx, 0, sizeof *ctx); memset(ctx, 0, sizeof *ctx);
@@ -536,7 +537,7 @@ processkey(Ictx *ctx, u32int sym, u32int state, char *text, int n, Keyres *res)
return 0; return 0;
ctx->focused = 1; ctx->focused = 1;
/* An IBus client drops a dead key we do not take: compose it here. */ /* An IBus client drops a dead key we do not take: compose it here. */
if(composekey(sym, text, n)) if(composekey(Composeibus, ctx, sym, text, n))
return -1; return -1;
if(text[0] != '\0') if(text[0] != '\0')
/* Composed text follows whatever was pending. */ /* Composed text follows whatever was pending. */

View File

@@ -3,24 +3,37 @@
#include "test.h" #include "test.h"
#include <xkbcommon/xkbcommon-keysyms.h> #include <xkbcommon/xkbcommon-keysyms.h>
/* A sequence swallows its keys and hands its text back at the end. */ /*
* A sequence swallows its keys and hands its text back at the end, and
* belongs to the one context, in the one frontend, that started it.
*/
void void
compose_sequences(struct ct *t) compose_sequences(struct ct *t)
{ {
static int a, b; /* two input contexts, told apart by address */
static const struct { static const struct {
int who;
void *owner;
u32int sym; u32int sym;
int composing; int composing;
char *text; char *text;
} keys[] = { } keys[] = {
{ XKB_KEY_a, 0, "" }, { Composexim, &a, XKB_KEY_a, 0, "" },
{ XKB_KEY_dead_acute, 1, "" }, { Composexim, &a, XKB_KEY_dead_acute, 1, "" },
{ XKB_KEY_e, 0, "é" }, { Composexim, &a, XKB_KEY_e, 0, "é" },
{ XKB_KEY_Multi_key, 1, "" }, { Composexim, &a, XKB_KEY_Multi_key, 1, "" },
{ XKB_KEY_o, 1, "" }, { Composexim, &a, XKB_KEY_o, 1, "" },
{ XKB_KEY_c, 0, "©" }, { Composexim, &a, XKB_KEY_c, 0, "©" },
{ XKB_KEY_dead_acute, 1, "" }, { Composexim, &a, XKB_KEY_dead_acute, 1, "" },
{ XKB_KEY_Escape, 1, "" }, { Composexim, &a, XKB_KEY_Escape, 1, "" },
{ XKB_KEY_a, 0, "" }, { Composexim, &a, XKB_KEY_a, 0, "" },
/* another context of the same frontend starts over */
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
{ Composexim, &b, XKB_KEY_e, 0, "" },
/* another frontend does not touch this one's sequence */
{ Composexim, &a, XKB_KEY_dead_acute, 1, "" },
{ Composewl, &a, XKB_KEY_o, 0, "" },
{ Composexim, &a, XKB_KEY_e, 0, "é" },
}; };
char buf[Maxutf]; char buf[Maxutf];
int i; int i;
@@ -29,9 +42,15 @@ compose_sequences(struct ct *t)
return; return;
composeinit(); composeinit();
for(i = 0; i < nelem(keys); i++){ for(i = 0; i < nelem(keys); i++){
CT_EQ_INT(t, keys[i].composing, CT_EQ_INT(t, keys[i].composing, composekey(keys[i].who,
composekey(keys[i].sym, buf, sizeof buf)); keys[i].owner, keys[i].sym, buf, sizeof buf));
CT_EQ_STR(t, keys[i].text, buf); CT_EQ_STR(t, keys[i].text, buf);
} }
/* a context that goes takes its half-typed sequence with it */
CT_EQ_INT(t, 1,
composekey(Composewl, &a, XKB_KEY_dead_acute, buf, sizeof buf));
composedrop(Composewl, &a);
CT_EQ_INT(t, 0, composekey(Composewl, &a, XKB_KEY_e, buf, sizeof buf));
CT_EQ_STR(t, "", buf);
unsetenv("XCOMPOSEFILE"); unsetenv("XCOMPOSEFILE");
} }

3
wl.c
View File

@@ -356,6 +356,7 @@ leave(void)
Keyres res; Keyres res;
releasekeys(); releasekeys();
composedrop(Composewl, &context);
if(engaged){ if(engaged){
sendrequest(Keyrelease, 0, 0, &res); sendrequest(Keyrelease, 0, 0, &res);
engaged = 0; engaged = 0;
@@ -506,7 +507,7 @@ grabkey(void*, struct zwp_input_method_keyboard_grab_v2*, u32int,
return; return;
} }
sym = xkb_state_key_get_one_sym(kstate, code + 8); sym = xkb_state_key_get_one_sym(kstate, code + 8);
if(composekey(sym, text, sizeof text)) if(composekey(Composewl, &context, sym, text, sizeof text))
return; /* a sequence in the making */ return; /* a sequence in the making */
key = ipckeysym(sym, xkb_keysym_to_utf32(sym)); key = ipckeysym(sym, xkb_keysym_to_utf32(sym));
if(keymeaningful(key)) if(keymeaningful(key))

3
xim.c
View File

@@ -384,7 +384,7 @@ kpress(Ic *state, xcb_key_press_event_t *ev)
int n; int n;
sym = keymaplookup(kstate, ev->detail, ev->state); sym = keymaplookup(kstate, ev->detail, ev->state);
if(composekey(sym, text, sizeof text)) if(composekey(Composexim, state, sym, text, sizeof text))
return; return;
key = ipckeysym(sym, xkb_keysym_to_utf32(sym)); key = ipckeysym(sym, xkb_keysym_to_utf32(sym));
if(keymeaningful(key)) if(keymeaningful(key))
@@ -424,6 +424,7 @@ icfree(void *p)
state = p; state = p;
/* The owner remains valid until imthread acknowledges its release. */ /* The owner remains valid until imthread acknowledges its release. */
release(state); release(state);
composedrop(Composexim, state);
icunlink(state); icunlink(state);
free(state); free(state);
} }