Zsh Mailing List Archive
Messages sorted by:
Reverse Date,
Date,
Thread,
Author
completion inserts a character from neither match; possible fix patch in join_sub()
- X-seq: zsh-workers 55139
- From: Luca Berneking <luca@xxxxxxxxxxxxx>
- To: zsh-workers@xxxxxxx
- Subject: completion inserts a character from neither match; possible fix patch in join_sub()
- Date: Tue, 08 Sep 2026 00:44:25 +0200
- Arc-authentication-results: i=1; strato.com; arc=none; dkim=none
- Arc-message-signature: i=1; a=rsa-sha256; c=relaxed/relaxed; t=1788821066; s=strato-dkim-0002; d=strato.com; h=Date:To:From:Subject:Message-ID:Cc:Date:From:Subject:Sender; bh=V5qlw+X8zO44o6HFxNsgO6/x4R1F9GwonJyE4LxLNBA=; b=boZ9oaP/0OyvR8V1N7FnoG34rRWXlXiDTCcYysHzA3hfPPAikRCvGFF5Zkb3cdH3wo wYNGqZO7IY66FA5LjGrDc+EkrL8wvBc1na4ML35+N4ZmiC/cpRcXRTfY4JXAhBWbPi5Z OFmrVrNDD7iTw+Ju/Ey5QKP3WJQSeiZOthC61Yf8TnXliUXRuaFeRvY98AxRxiJbqYJp GiDFq9LJoYXUlKeVSKBjP6PnxGrT2u1LKb6Ss8RKEy9aEtIwhfL3mrBH10KCRW+dDeCo 7aI8ES8KjIY+Y97SHrR7SCkBAZIufmppQeYy975dglqNensLsgMrVPxeeoQiksJ4fZld dNSQ==
- Arc-seal: i=1; a=rsa-sha256; t=1788821066; cv=none; d=strato.com; s=strato-dkim-0002; b=b543DXph9KOK2RLpHaVCaGgmkdKYMkB0Igb/ixFVFTSa52nEK65z4lz1buZ24QcrrX vGuxYw5JFVRsMolYdOWVHNAB5tIuXkfJ34C1Vu+0xS9fjOMfKw4v5X8ww0Nl7phrqqud wUQEPTz5dl0ojxVh+Q1YdO49ThiP2h/BkAD4e6zYl4L3EMil81Jjwv4bIH93tdRPY4H+ KnQouIksofVql47c3aiTo5b8EzDLOLkllq1tArXdhYEXKmQEKx1YdmKCH8pSG8r9I6no SbjoNktdp2O1mwrDAoy1KxAnbzr4rmmlD8wuOMLamQiqeTN5yRFMWxDd7UWp6Q0P3WkK 37cA==
- Archived-at: <https://zsh.org/workers/55139>
- List-id: <zsh-workers.zsh.org>
Hi,
I am facing an annoying autocomplete bug with the default ohmyzsh config in some of my directories.
At first I thought that this is related to the ohmyzsh configuration but finally had the time to
take a closer look at the bug with the help of an LLM. My original ohmyzsh github issue:
https://github.com/ohmyzsh/ohmyzsh/issues/12723
Reproducers tested with zsh 5.9.2:
autoload -Uz compinit; compinit -u
zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' 'r:|=*' 'l:|=* r:|=*'
Now
mkdir -p /tmp/x && cd /tmp/x && touch ab-xyFOO ac-xyBAR
cat -xy<TAB>
autocompletes to `aB-xy`, and
mkdir -p /tmp/t && cd /tmp/t && touch 1abc 2abc
cat a<TAB>
autocompletes to `Aabc`.
I found similar bug reports from 2014 (X-seq: zsh-users 18877).
The bug got semi-fixed in zsh 5.9
https://www.zsh.org/mla/workers/2021/msg01887.html
https://www.zsh.org/mla/workers/2016/msg00671.html
but got reverted due to a regression with
https://www.zsh.org/mla/workers/2022/msg00652.html
https://www.zsh.org/mla/workers/2022/msg00655.html
I dont have any prior experience with the zsh source code but might have found the bug using an LLM:
> join_sub() (Src/Zle/compmatch.c) compares two chunks of text and asks
> whether one string could stand for both. The old chunk arrives from the
> caller as str/len; the new one is tracked by the cursor struct as
> md->str/md->len.
>
> A chunk can be identified by where it starts or where it ends, and which
> is wanted depends on direction: when sfx is set, pattern_match() scans
> backwards and needs pointers to the end. That is what the "if (sfx)"
> block is for. But the two chunks do not arrive in the same form -- str
> from the caller is a start pointer, while md->str is already an end
> pointer, because check_cmdata() did "md->str += md->len" when it loaded
> the chunk.
>
> There are two mistakes, and both have to be fixed; correcting either one
> alone leaves the function broken.
>
> 1. The "if (sfx)" block treats the two sides symmetrically:
>
> ow += ol; nw += nl;
>
> That is right for ow, which really is a start, and wrong for nw, which
> was already an end. Advancing it again pushes it a whole region-length
> too far, past the chunk being compared and into whatever follows. For
> 1abc/2abc that is the character after the anchor. In other cases it
> runs off the end of the allocation: for two directories ppaazz and
> ppbbzz it lands at offset 8 of a 6-byte string, so some of the
> currently "correct" answers in this area are only correct because the
> out-of-bounds bytes happened not to match.
>
> 2. Those same pointers are then handed to bld_line(), which walks its
> "word" argument forwards for wlen characters to build a character
> array, and so needs the start of the region. It was being given the
> end. This is independent of (1): even with nw correct as an end
> pointer, it is still the wrong thing to pass here.
>
> Instrumented trace of the 1abc/2abc case, before the fix:
>
> join_sub ENTER: sfx=4 ow=<1> ol=1 | md.pcl.word=<2>(wlen=1) str_off=1
> join_sub: after sfx adj: nw points past <a>
> pattern_match1: EQUIV chr='1' -> NO MATCH, returning 0
> pattern_match1: EQUIV chr='2' -> NO MATCH, returning 0
> join_sub: matched t=0 -> mw=<a> other=<1>
> bld_line ENTER: mword=<a> word=<a> wlen=1 sfx=4
> pme: wind=27 -> PATMATCHINDEX index=26 -> exact lchr='A'
> bld_line EXIT: rl=1 built-line=<A>
>
> '1' and '2' are correctly rejected as having no equivalent, which is
> where it should have stopped. The 'A' comes from then comparing 'a' --
> the character after the anchor in 2abc -- against '1'. bld_line() and
> pattern_match_equivalence() are behaving correctly on the input they are
> given; they are simply given the wrong characters.
diff --git a/Src/Zle/compmatch.c b/Src/Zle/compmatch.c
index 7e0411294..5bafa18a9 100644
--- a/Src/Zle/compmatch.c
+++ b/Src/Zle/compmatch.c
@@ -2212,12 +2212,23 @@ join_sub(Cmdata md, char *str, int len, int *mlen, int sfx, int join)
if (!check_cmdata(md, sfx)) {
char *ow = str, *nw = md->str;
int ol = len, nl = md->len;
+ /*
+ * ow and nw are used below as the *end* of each unmatched region,
+ * since pattern_match() scans backwards from there when sfx is
+ * set. bld_line() instead reads its "word" argument forwards and
+ * needs the *start*, derived here the same way undo_cmdata() does.
+ */
+ char *owb = str, *nwb = md->str - (sfx ? md->len : 0);
Cmlist ms;
Cmatcher mp;
int t;
if (sfx) {
- ow += ol; nw += nl;
+ /*
+ * check_cmdata() has already positioned md->str at the end of
+ * the unconsumed portion, so nw must not be advanced again.
+ */
+ ow += ol;
}
for (t = 0, ms = bmatchers; ms && !t; ms = ms->next) {
mp = ms->matcher;
@@ -2262,7 +2273,7 @@ join_sub(Cmdata md, char *str, int len, int *mlen, int sfx, int join)
else
mw = nw - (sfx ? mp->wlen : 0);
- if ((bl = bld_line(mp, line, mw, (t ? nw : ow),
+ if ((bl = bld_line(mp, line, mw, (t ? nwb : owb),
(t ? nl : ol), sfx))) {
/* Yep, one of the lines matched the other
* string. */
Messages sorted by:
Reverse Date,
Date,
Thread,
Author