summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTom Lane2026-08-29 23:56:49 +0000
committerTom Lane2026-08-29 23:56:49 +0000
commitb4c8afdfaf52ce4a5839be6b4d205e696b00a543 (patch)
treecdf2a9ed85bd717b8b5c3facfa330fbdee08c2ac /src
parent978caef1936fc06a6dcc75283115ede953ab6d34 (diff)
Harden spell.c against out-of-order FLAG lines in Hunspell files.
The compound flags collected from COMPOUNDFLAG and friends are stored in either the string or the integer member of a union, according to the flag mode that the affix file's FLAG line declares. NIImportOOAffixes() converted each flag as soon as it read it, using the mode in effect at that point, and recorded that mode in the entry. Since FLAG may appear anywhere in the file, including after the compound flags, entries written before and after it could disagree about which member of the union holds the flag. In assert-enabled builds, this would result in an assertion failure. Otherwise, cmpcmdflag() takes the mode from its first argument and applies it to both, so it can read an integer as a char pointer and pass that to strcmp(). Depending on which way the mismatch goes, the result is a segfault while sorting the array, a segfault in the bsearch() that later looks flags up (the lookup key is built with the final mode, so this happens even when the array itself is consistent), or, when both members happen to be readable, no crash at all and a compound flag that is never found, which silently disables compound word splitting. This isn't a security bug because we consider dictionary files to be trusted data, but it's still worth fixing. (In practice, dictionary files usually put the FLAG line first, which is why this went unreported for so long.) Fix by keeping the flags as strings while the file is read and converting them once it has been read in full, when the mode is final. This also makes the position of the FLAG line irrelevant, which is how the flags on AF, SFX and PFX lines are already treated: those are parsed in a second pass and so always use the final mode. That precedent is reason for behaving this way rather than throwing an error. The old ispell file format reaches addCompoundAffixFlagValue() too, from NIImportAffixes(), and returns without entering NIImportOOAffixes(), so it needs the conversion step as well. While we're here, also fix some integer width mismatches: store the result of strtol() into a "long", and cast to int only after we've done range checks. Typically a value too wide for int would fail the range checks anyway, but in some cases it would be silently accepted after truncation to int. Author: Ewan Young <kdbase.hack@gmail.com> Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us> Discussion: https://postgr.es/m/CAON2xHN3QmsaySM6DGWa1gttcbJoFh0wjAE-_ZpSPo=LKN1hYw@mail.gmail.com Backpatch-through: 14
Diffstat (limited to 'src')
-rw-r--r--src/backend/tsearch/spell.c115
1 files changed, 88 insertions, 27 deletions
diff --git a/src/backend/tsearch/spell.c b/src/backend/tsearch/spell.c
index b4b90e5b969..3a0da19e573 100644
--- a/src/backend/tsearch/spell.c
+++ b/src/backend/tsearch/spell.c
@@ -348,7 +348,7 @@ cmpaffix(const void *s1, const void *s2)
static void
getNextFlagFromString(IspellDict *Conf, char **sflagset, char *sflag)
{
- int32 s;
+ long sval;
char *next,
*sbuf = *sflagset;
int maxstep;
@@ -376,17 +376,17 @@ getNextFlagFromString(IspellDict *Conf, char **sflagset, char *sflag)
break;
case FM_NUM:
errno = 0;
- s = strtol(*sflagset, &next, 10);
+ sval = strtol(*sflagset, &next, 10);
if (*sflagset == next || errno == ERANGE)
ereport(ERROR,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("invalid affix flag \"%s\"", *sflagset)));
- if (s < 0 || s > FLAGNUM_MAXSIZE)
+ if (sval < 0 || sval > FLAGNUM_MAXSIZE)
ereport(ERROR,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("affix flag \"%s\" is out of range",
*sflagset)));
- sflag += sprintf(sflag, "%0d", s);
+ sflag += sprintf(sflag, "%0d", (int) sval);
/* Go to start of the next flag */
*sflagset = next;
@@ -1053,30 +1053,40 @@ parse_affentry(const char *str, char *mask, char *find, char *repl)
}
/*
+ * Parse an affix flag written in the "num" flag mode.
+ */
+static uint32
+parseNumericAffixFlag(const char *s)
+{
+ char *next;
+ long i;
+
+ errno = 0;
+ i = strtol(s, &next, 10);
+ if (s == next || errno == ERANGE)
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("invalid affix flag \"%s\"", s)));
+ if (i < 0 || i > FLAGNUM_MAXSIZE)
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("affix flag \"%s\" is out of range", s)));
+
+ return (uint32) i;
+}
+
+/*
* Sets a Hunspell options depending on flag type.
+ *
+ * Conf->flagMode must already have its final value, since it decides which
+ * member of the entry's union is written. See finalizeCompoundAffixFlags().
*/
static void
setCompoundAffixFlagValue(IspellDict *Conf, CompoundAffixFlag *entry,
- char *s, uint32 val)
+ const char *s, uint32 val)
{
if (Conf->flagMode == FM_NUM)
- {
- char *next;
- int i;
-
- errno = 0;
- i = strtol(s, &next, 10);
- if (s == next || errno == ERANGE)
- ereport(ERROR,
- (errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("invalid affix flag \"%s\"", s)));
- if (i < 0 || i > FLAGNUM_MAXSIZE)
- ereport(ERROR,
- (errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("affix flag \"%s\" is out of range", s)));
-
- entry->flag.i = i;
- }
+ entry->flag.i = parseNumericAffixFlag(s);
else
entry->flag.s = cpstrdup(Conf, s);
@@ -1139,13 +1149,55 @@ addCompoundAffixFlagValue(IspellDict *Conf, const char *s, uint32 val)
newValue = Conf->CompoundAffixFlags + Conf->nCompoundAffixFlag;
- setCompoundAffixFlagValue(Conf, newValue, sbuf, val);
+ /*
+ * Only remember the flag as a string for now. The FLAG option that says
+ * how flags are spelled may appear anywhere in the affix file, including
+ * after the compound flags themselves, so the final representation cannot
+ * be chosen until the whole file has been read. See
+ * finalizeCompoundAffixFlags(), which fills in flagMode as well.
+ *
+ * The interim copy goes in the short-lived build context, since the final
+ * representation may well not be a string at all.
+ */
+ newValue->flag.s = MemoryContextStrdup(Conf->buildCxt, sbuf);
+ newValue->value = val;
Conf->usecompound = true;
Conf->nCompoundAffixFlag++;
}
/*
+ * Convert the compound flags collected by addCompoundAffixFlagValue() to the
+ * representation implied by the flag mode the affix file ended up declaring.
+ *
+ * This must run before the flags are sorted or searched. Doing the conversion
+ * here rather than while reading the file makes the position of the FLAG line
+ * irrelevant, which is how the flags on AF, SFX and PFX lines are already
+ * treated: those are parsed in a second pass over the file, and so always use
+ * the final flag mode.
+ */
+static void
+finalizeCompoundAffixFlags(IspellDict *Conf)
+{
+ for (int i = 0; i < Conf->nCompoundAffixFlag; i++)
+ {
+ CompoundAffixFlag *entry = Conf->CompoundAffixFlags + i;
+
+ /*
+ * Replace the interim string with the representation the flag mode
+ * calls for. In both cases the old value is read before the new one
+ * is stored, so overwriting the union in place is safe.
+ */
+ if (Conf->flagMode == FM_NUM)
+ entry->flag.i = parseNumericAffixFlag(entry->flag.s);
+ else
+ entry->flag.s = cpstrdup(Conf, entry->flag.s);
+
+ entry->flagMode = Conf->flagMode;
+ }
+}
+
+/*
* Returns a set of affix parameters which correspondence to the set of affix
* flags s.
*/
@@ -1190,7 +1242,7 @@ getAffixFlagSet(IspellDict *Conf, char *s)
{
if (Conf->useFlagAliases && *s != '\0')
{
- int curaffix;
+ long curaffix;
char *end;
errno = 0;
@@ -1321,6 +1373,9 @@ NIImportOOAffixes(IspellDict *Conf, const char *filename)
}
tsearch_readline_end(&trst);
+ /* Conf->flagMode is final now, so the compound flags can be converted */
+ finalizeCompoundAffixFlags(Conf);
+
if (Conf->nCompoundAffixFlag > 1)
qsort((void *) Conf->CompoundAffixFlags, Conf->nCompoundAffixFlag,
sizeof(CompoundAffixFlag), cmpcmdflag);
@@ -1595,6 +1650,12 @@ nextline:
pfree(pstr);
}
tsearch_readline_end(&trst);
+
+ /*
+ * The old file format has no FLAG command, so the mode is still FM_CHAR
+ * here, but the flags collected above must be converted all the same.
+ */
+ finalizeCompoundAffixFlags(Conf);
return;
isnewformat:
@@ -1763,7 +1824,7 @@ NISortDictionary(IspellDict *Conf)
{
int i;
int naffix;
- int curaffix;
+ long curaffix;
/* compress affixes */
@@ -1806,7 +1867,7 @@ NISortDictionary(IspellDict *Conf)
curaffix = 0;
}
- Conf->Spell[i]->p.d.affix = curaffix;
+ Conf->Spell[i]->p.d.affix = (int) curaffix;
Conf->Spell[i]->p.d.len = strlen(Conf->Spell[i]->word);
}
}
@@ -1844,7 +1905,7 @@ NISortDictionary(IspellDict *Conf)
Conf->Spell[i]->p.flag);
}
- Conf->Spell[i]->p.d.affix = curaffix;
+ Conf->Spell[i]->p.d.affix = (int) curaffix;
Conf->Spell[i]->p.d.len = strlen(Conf->Spell[i]->word);
}