agora inbox for pgsql-bugs@postgresql.org
help / color / mirror / Atom feedBUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3
15+ messages / 5 participants
[nested] [flat]
* BUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3
@ 2026-08-02 01:10 PG Bug reporting form <noreply@postgresql.org>
0 siblings, 1 reply; 15+ messages in thread
From: PG Bug reporting form @ 2026-08-02 01:10 UTC (permalink / raw)
To: pgsql-bugs@lists.postgresql.org; +Cc: michaelmalis2@gmail.com
The following bug has been logged on the website:
Bug reference: 19595
Logged by: Michael Malis
Email address: michaelmalis2@gmail.com
PostgreSQL version: 18.3
Operating system: MacOS
Description:
(I initially filed this at security@ but because the dictionary is
considered a trusted
file Tom asked me to repost here)
Three memory-safety defects in the ispell/hunspell dictionary loader, all
reached by CREATE TEXT SEARCH DICTIONARY on a malformed dictionary file.
BUG 1 -- out-of-bounds heap write in NISortAffixes()
1987: Conf->CompoundAffix = ptr = (CMPDAffix *)
palloc(sizeof(CMPDAffix) * Conf->naffixes);
... /* loop over i < naffixes; ptr++ once per collected affix */
2015: ptr->affix = NULL;
2016: Conf->CompoundAffix = repalloc(Conf->CompoundAffix,
sizeof(CMPDAffix) * (ptr - Conf->CompoundAffix + 1));
The array holds exactly naffixes elements. When every affix is collected,
ptr == base + naffixes, so line 2015 writes one element (8 bytes) past the
end -- and the repalloc that would make room for the terminator is on the
next line, after the write. CMPDAffix is 16 bytes, palloc rounds to
power-of-two chunks, so the write escapes its chunk when naffixes is a power
of two, and always escapes for naffixes > 512 (dedicated block), which
covers typical dictionaries. The store then lands in the next chunk's header
or in malloc metadata.
Reproducer -- in $SHAREDIR/tsearch_data, oob.affix:
compoundwords controlled Z
suffixes
flag ~Z:
. > S
oob.dict:
foo/Z
then:
CREATE TEXT SEARCH DICTIONARY oob (TEMPLATE = ispell, DictFile =
oob, AffFile = oob);
SELECT ts_lexize('oob', 'foos');
This gives naffixes == 1 with the one affix collected. The allocator detects
the damage at the next allocation ("free list is damaged", aborting in
palloc from mkANode from NISortAffixes).
Fix: allocate naffixes + 1 at line 1987, or write the terminator after the
repalloc.
BUG 2 -- uninitialized stack buffer read in NIImportAffixes()
1426: char flag[BUFSIZ]; /* never initialized */
...
1519: flag[0] = *s++; /* only written in the "flag" branch
*/
1520: flag[1] = '\0';
...
1543: NIAddAffix(Conf, flag, flagflags, mask, find, repl, ...);
/* unconditional */
flag is written only inside the "flag" directive branch but passed
unconditionally to NIAddAffix, which does cpstrdup(Conf, flag) -- strlen +
strcpy over uninitialized stack. If no "flag" line was parsed, this is an
unbounded strlen (no guaranteed NUL in BUFSIZ) and stack contents are copied
into a long-lived dictionary flag.
Trigger: a .affix file with an old-format "prefixes"/"suffixes" section and
a parseable affix entry but no "flag" directive, e.g.:
COMPOUNDWORDS l 1
suffixes
nlag Z:
. > S
with dict "foo/Z" ("nlag" is simply not "flag", so the directive is never
seen while the entry still parses).
Fix: initialize flag[0] = '\0' at declaration.
BUG 3 -- NULL dereference on unfilled AF alias slots
1324: Conf->AffixData = (const char **) palloc0(naffix * sizeof(char *));
1336: Conf->AffixData[curaffix] = cpstrdup(Conf, sflag); /* one
per AF line */
The alias table is zero-filled and only the AF lines actually present are
filled. If "AF <n>" declares more slots than the file fills, the tail stays
NULL, and those NULL slots are dereferenced without a check:
- MergeAffix() line 1576: if (*Conf->AffixData[a1] == '\0')
(the Assert at 1573 checks only the index, and asserts are off in
production builds), reached from NISortDictionary() at line 1691;
- getAffixFlagSet() (1156) -> getCompoundAffixFlagValue() (1120) ->
getNextFlagFromString() (350), reached from inside NIImportOOAffixes().
Trigger: a .affix file whose AF table declares a larger count than the
number of AF lines it provides, with a dictionary word referencing an
unfilled index.
Fix: reject an incompletely populated AF table, or treat a NULL slot as the
empty flag set (VoidString) at the dereference sites.
Reachability: CREATE TEXT SEARCH DICTIONARY requires CREATE on a schema, not
superuser (DefineTSDictionary in src/backend/commands/tsearchcmds.c). The
file path is restricted to [a-z0-9_] under $SHAREDIR/tsearch_data
(get_tsearch_config_filename in src/backend/tsearch/ts_utils.c), so the
crafted file must be placed there by other means -- most realistically a
corrupt or hostile third-party hunspell dictionary. Minimized file pairs
available on request.
^ permalink raw reply [nested|flat] 15+ messages in thread
* Re: BUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3
@ 2026-08-02 07:29 Andrey Rachitskiy <pl0h0yp1@gmail.com>
parent: PG Bug reporting form <noreply@postgresql.org>
0 siblings, 1 reply; 15+ messages in thread
From: Andrey Rachitskiy @ 2026-08-02 07:29 UTC (permalink / raw)
To: michaelmalis2@gmail.com; pgsql-bugs@lists.postgresql.org
Hi, Michael!
Patch attached.
1. NISortAffixes() — allocate CompoundAffix with naffixes + 1 so the
terminating NULL entry fits. The previous allocation of exactly
naffixes elements left the terminator one past the end when every
affix was collected.
2. NIImportAffixes() — initialize flag[0] = '\0' so NIAddAffix() does
not see an uninitialized buffer when an old-format affix entry
appears without a preceding "flag" directive.
3. Incomplete Hunspell AF tables — reject a NULL AffixData slot in
getAffixFlagSet() as an invalid affix alias (same error text as an
out-of-range alias), and reject an incompletely populated AF table
at the end of NIImportOOAffixes(), mirroring the existing "too many
aliases" check. The former covers a mid-parse reference to an
unfilled slot, the latter covers a truncated AF table with no such
reference.
Regression tests cover both AF cases. The former errors while
tsearch_readline() still has an error-context callback installed, so
the ERROR would otherwise include CONTEXT with the absolute path of
the affix file under $SHAREDIR. That path is install-dependent and
would make the expected file non-portable, so the test wraps that
CREATE in \set VERBOSITY terse (same pattern as elsewhere in the
regress suite). The incomplete-table case errors after
tsearch_readline_end(), so it needs no VERBOSITY tweak.
вс, 2 авг. 2026 г. в 08:30, PG Bug reporting form <noreply@postgresql.org>:
> The following bug has been logged on the website:
>
> Bug reference: 19595
> Logged by: Michael Malis
> Email address: michaelmalis2@gmail.com
> PostgreSQL version: 18.3
> Operating system: MacOS
> Description:
>
> (I initially filed this at security@ but because the dictionary is
> considered a trusted
> file Tom asked me to repost here)
>
> Three memory-safety defects in the ispell/hunspell dictionary loader, all
> reached by CREATE TEXT SEARCH DICTIONARY on a malformed dictionary file.
>
> BUG 1 -- out-of-bounds heap write in NISortAffixes()
>
> 1987: Conf->CompoundAffix = ptr = (CMPDAffix *)
> palloc(sizeof(CMPDAffix) * Conf->naffixes);
> ... /* loop over i < naffixes; ptr++ once per collected affix */
> 2015: ptr->affix = NULL;
> 2016: Conf->CompoundAffix = repalloc(Conf->CompoundAffix,
> sizeof(CMPDAffix) * (ptr - Conf->CompoundAffix + 1));
>
> The array holds exactly naffixes elements. When every affix is collected,
> ptr == base + naffixes, so line 2015 writes one element (8 bytes) past the
> end -- and the repalloc that would make room for the terminator is on the
> next line, after the write. CMPDAffix is 16 bytes, palloc rounds to
> power-of-two chunks, so the write escapes its chunk when naffixes is a
> power
> of two, and always escapes for naffixes > 512 (dedicated block), which
> covers typical dictionaries. The store then lands in the next chunk's
> header
> or in malloc metadata.
>
> Reproducer -- in $SHAREDIR/tsearch_data, oob.affix:
>
> compoundwords controlled Z
> suffixes
> flag ~Z:
> . > S
>
> oob.dict:
>
> foo/Z
>
> then:
>
> CREATE TEXT SEARCH DICTIONARY oob (TEMPLATE = ispell, DictFile =
> oob, AffFile = oob);
> SELECT ts_lexize('oob', 'foos');
>
> This gives naffixes == 1 with the one affix collected. The allocator
> detects
> the damage at the next allocation ("free list is damaged", aborting in
> palloc from mkANode from NISortAffixes).
>
> Fix: allocate naffixes + 1 at line 1987, or write the terminator after the
> repalloc.
>
>
> BUG 2 -- uninitialized stack buffer read in NIImportAffixes()
>
> 1426: char flag[BUFSIZ]; /* never initialized */
> ...
> 1519: flag[0] = *s++; /* only written in the "flag"
> branch
> */
> 1520: flag[1] = '\0';
> ...
> 1543: NIAddAffix(Conf, flag, flagflags, mask, find, repl, ...);
> /* unconditional */
>
> flag is written only inside the "flag" directive branch but passed
> unconditionally to NIAddAffix, which does cpstrdup(Conf, flag) -- strlen +
> strcpy over uninitialized stack. If no "flag" line was parsed, this is an
> unbounded strlen (no guaranteed NUL in BUFSIZ) and stack contents are
> copied
> into a long-lived dictionary flag.
>
> Trigger: a .affix file with an old-format "prefixes"/"suffixes" section and
> a parseable affix entry but no "flag" directive, e.g.:
>
> COMPOUNDWORDS l 1
> suffixes
> nlag Z:
> . > S
>
> with dict "foo/Z" ("nlag" is simply not "flag", so the directive is never
> seen while the entry still parses).
>
> Fix: initialize flag[0] = '\0' at declaration.
>
>
> BUG 3 -- NULL dereference on unfilled AF alias slots
>
> 1324: Conf->AffixData = (const char **) palloc0(naffix * sizeof(char
> *));
> 1336: Conf->AffixData[curaffix] = cpstrdup(Conf, sflag); /* one
> per AF line */
>
> The alias table is zero-filled and only the AF lines actually present are
> filled. If "AF <n>" declares more slots than the file fills, the tail stays
> NULL, and those NULL slots are dereferenced without a check:
>
> - MergeAffix() line 1576: if (*Conf->AffixData[a1] == '\0')
> (the Assert at 1573 checks only the index, and asserts are off in
> production builds), reached from NISortDictionary() at line 1691;
> - getAffixFlagSet() (1156) -> getCompoundAffixFlagValue() (1120) ->
> getNextFlagFromString() (350), reached from inside NIImportOOAffixes().
>
> Trigger: a .affix file whose AF table declares a larger count than the
> number of AF lines it provides, with a dictionary word referencing an
> unfilled index.
>
> Fix: reject an incompletely populated AF table, or treat a NULL slot as the
> empty flag set (VoidString) at the dereference sites.
>
> Reachability: CREATE TEXT SEARCH DICTIONARY requires CREATE on a schema,
> not
> superuser (DefineTSDictionary in src/backend/commands/tsearchcmds.c). The
> file path is restricted to [a-z0-9_] under $SHAREDIR/tsearch_data
> (get_tsearch_config_filename in src/backend/tsearch/ts_utils.c), so the
> crafted file must be placed there by other means -- most realistically a
> corrupt or hostile third-party hunspell dictionary. Minimized file pairs
> available on request.
>
>
>
>
>
Attachments:
[text/x-patch] 0001-Fix-ispell-memsafe-REL_19_STABLE.patch (6.9K, ../../CAB8bMiun+cTqTnv-cTfxvTbnOLxWunRgyZeUZ88YMSLLXub4mg@mail.gmail.com/3-0001-Fix-ispell-memsafe-REL_19_STABLE.patch)
download | inline diff:
From 4392739e612d8c20f8c775de9c4b5fa8036b39df Mon Sep 17 00:00:00 2001
From: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Date: Sun, 2 Aug 2026 12:10:47 +0500
Subject: [PATCH] Fix memory-safety bugs in the ispell/hunspell dictionary
loader.
Allocate CompoundAffix with room for its terminator, initialize the
old-format flag buffer before NIAddAffix(), and reject incomplete or
NULL Hunspell AF alias slots. Add regression tests for the AF cases.
Author: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Reported-by: Michael Malis <michaelmalis2@gmail.com>
Discussion: https://www.postgresql.org/message-id/19595-7dc18b4e212c4757%40postgresql.org
---
src/backend/tsearch/Makefile | 4 +++-
.../tsearch/dicts/hunspell_test_afshort.affix | 5 +++++
.../tsearch/dicts/hunspell_test_afshort.dict | 1 +
.../tsearch/dicts/hunspell_test_aftrunc.affix | 5 +++++
.../tsearch/dicts/hunspell_test_aftrunc.dict | 1 +
src/backend/tsearch/spell.c | 18 +++++++++++++++++-
src/test/regress/expected/tsdicts.out | 16 ++++++++++++++++
src/test/regress/sql/tsdicts.sql | 16 ++++++++++++++++
8 files changed, 64 insertions(+), 2 deletions(-)
create mode 100644 src/backend/tsearch/dicts/hunspell_test_afshort.affix
create mode 100644 src/backend/tsearch/dicts/hunspell_test_afshort.dict
create mode 100644 src/backend/tsearch/dicts/hunspell_test_aftrunc.affix
create mode 100644 src/backend/tsearch/dicts/hunspell_test_aftrunc.dict
diff --git a/src/backend/tsearch/Makefile b/src/backend/tsearch/Makefile
index 4a436150109..9430d906122 100644
--- a/src/backend/tsearch/Makefile
+++ b/src/backend/tsearch/Makefile
@@ -18,7 +18,9 @@ DICTFILES=synonym_sample.syn thesaurus_sample.ths \
hunspell_sample.affix \
ispell_sample.affix ispell_sample.dict \
hunspell_sample_long.affix hunspell_sample_long.dict \
- hunspell_sample_num.affix hunspell_sample_num.dict
+ hunspell_sample_num.affix hunspell_sample_num.dict \
+ hunspell_test_afshort.affix hunspell_test_afshort.dict \
+ hunspell_test_aftrunc.affix hunspell_test_aftrunc.dict
# Local paths to dictionaries files
DICTFILES_PATH=$(addprefix dicts/,$(DICTFILES))
diff --git a/src/backend/tsearch/dicts/hunspell_test_afshort.affix b/src/backend/tsearch/dicts/hunspell_test_afshort.affix
new file mode 100644
index 00000000000..df0656b7b50
--- /dev/null
+++ b/src/backend/tsearch/dicts/hunspell_test_afshort.affix
@@ -0,0 +1,5 @@
+COMPOUNDFLAG Z
+AF 2
+AF x
+SFX A Y 1
+SFX A 0 0/2 .
diff --git a/src/backend/tsearch/dicts/hunspell_test_afshort.dict b/src/backend/tsearch/dicts/hunspell_test_afshort.dict
new file mode 100644
index 00000000000..8c5c4c3f000
--- /dev/null
+++ b/src/backend/tsearch/dicts/hunspell_test_afshort.dict
@@ -0,0 +1 @@
+foo/2
diff --git a/src/backend/tsearch/dicts/hunspell_test_aftrunc.affix b/src/backend/tsearch/dicts/hunspell_test_aftrunc.affix
new file mode 100644
index 00000000000..f1b6e5b9513
--- /dev/null
+++ b/src/backend/tsearch/dicts/hunspell_test_aftrunc.affix
@@ -0,0 +1,5 @@
+AF 2
+AF x
+
+SFX A Y 1
+SFX A 0 s .
diff --git a/src/backend/tsearch/dicts/hunspell_test_aftrunc.dict b/src/backend/tsearch/dicts/hunspell_test_aftrunc.dict
new file mode 100644
index 00000000000..257cc5642cb
--- /dev/null
+++ b/src/backend/tsearch/dicts/hunspell_test_aftrunc.dict
@@ -0,0 +1 @@
+foo
diff --git a/src/backend/tsearch/spell.c b/src/backend/tsearch/spell.c
index 15dccb47bf5..f8454c5b89c 100644
--- a/src/backend/tsearch/spell.c
+++ b/src/backend/tsearch/spell.c
@@ -1182,12 +1182,18 @@ getAffixFlagSet(IspellDict *Conf, char *s)
errmsg("invalid affix alias \"%s\"", s)));
if (curaffix > 0 && curaffix < Conf->nAffixData)
+ {
+ if (Conf->AffixData[curaffix] == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("invalid affix alias \"%s\"", s)));
/*
* Do not subtract 1 from curaffix because empty string was added
* in NIImportOOAffixes
*/
return Conf->AffixData[curaffix];
+ }
else if (curaffix > Conf->nAffixData)
ereport(ERROR,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
@@ -1422,6 +1428,13 @@ nextline:
tsearch_readline_end(&trst);
if (ptype)
pfree(ptype);
+
+ /* Reject incomplete AF alias table. */
+ if (Conf->useFlagAliases && curaffix != naffix)
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("too few flag vector aliases (expected %d)",
+ naffix - 1)));
}
/*
@@ -1449,6 +1462,8 @@ NIImportAffixes(IspellDict *Conf, const char *filename)
bool oldformat = false;
char *recoded = NULL;
+ flag[0] = '\0'; /* no flag seen yet */
+
if (!tsearch_readline_begin(&trst, filename))
ereport(ERROR,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
@@ -1998,7 +2013,8 @@ NISortAffixes(IspellDict *Conf)
/* Store compound affixes in the Conf->CompoundAffix array */
if (Conf->naffixes > 1)
qsort(Conf->Affix, Conf->naffixes, sizeof(AFFIX), cmpaffix);
- Conf->CompoundAffix = ptr = palloc_array(CMPDAffix, Conf->naffixes);
+ /* +1 for terminator */
+ Conf->CompoundAffix = ptr = palloc_array(CMPDAffix, Conf->naffixes + 1);
ptr->affix = NULL;
for (i = 0; i < Conf->naffixes; i++)
diff --git a/src/test/regress/expected/tsdicts.out b/src/test/regress/expected/tsdicts.out
index 0bbf2ff4ca2..fc56de6cc50 100644
--- a/src/test/regress/expected/tsdicts.out
+++ b/src/test/regress/expected/tsdicts.out
@@ -447,6 +447,22 @@ CREATE TEXT SEARCH DICTIONARY hunspell_err (
AffFile=hunspell_sample_long
);
ERROR: invalid affix alias "302,301,202,303"
+-- incomplete AF table (alias references an unfilled slot)
+\set VERBOSITY terse
+CREATE TEXT SEARCH DICTIONARY hunspell_test_afshort (
+ Template=ispell,
+ DictFile=hunspell_test_afshort,
+ AffFile=hunspell_test_afshort
+);
+ERROR: invalid affix alias "2"
+\set VERBOSITY default
+-- incomplete AF table (no reference to the missing slot)
+CREATE TEXT SEARCH DICTIONARY hunspell_test_aftrunc (
+ Template=ispell,
+ DictFile=hunspell_test_aftrunc,
+ AffFile=hunspell_test_aftrunc
+);
+ERROR: too few flag vector aliases (expected 2)
-- Synonym dictionary
CREATE TEXT SEARCH DICTIONARY synonym (
Template=synonym,
diff --git a/src/test/regress/sql/tsdicts.sql b/src/test/regress/sql/tsdicts.sql
index cf08410bb2d..9bb2cd1a848 100644
--- a/src/test/regress/sql/tsdicts.sql
+++ b/src/test/regress/sql/tsdicts.sql
@@ -138,6 +138,22 @@ CREATE TEXT SEARCH DICTIONARY hunspell_err (
AffFile=hunspell_sample_long
);
+-- incomplete AF table (alias references an unfilled slot)
+\set VERBOSITY terse
+CREATE TEXT SEARCH DICTIONARY hunspell_test_afshort (
+ Template=ispell,
+ DictFile=hunspell_test_afshort,
+ AffFile=hunspell_test_afshort
+);
+\set VERBOSITY default
+
+-- incomplete AF table (no reference to the missing slot)
+CREATE TEXT SEARCH DICTIONARY hunspell_test_aftrunc (
+ Template=ispell,
+ DictFile=hunspell_test_aftrunc,
+ AffFile=hunspell_test_aftrunc
+);
+
-- Synonym dictionary
CREATE TEXT SEARCH DICTIONARY synonym (
Template=synonym,
--
2.53.0
[text/x-patch] 0001-Fix-ispell-memsafe-master.patch (6.9K, ../../CAB8bMiun+cTqTnv-cTfxvTbnOLxWunRgyZeUZ88YMSLLXub4mg@mail.gmail.com/4-0001-Fix-ispell-memsafe-master.patch)
download | inline diff:
From e6047960e96e2a3bdd5d8b6ca1308203d9ea5a5f Mon Sep 17 00:00:00 2001
From: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Date: Sun, 2 Aug 2026 12:07:36 +0500
Subject: [PATCH] Fix memory-safety bugs in the ispell/hunspell dictionary
loader.
Allocate CompoundAffix with room for its terminator, initialize the
old-format flag buffer before NIAddAffix(), and reject incomplete or
NULL Hunspell AF alias slots. Add regression tests for the AF cases.
Author: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Reported-by: Michael Malis <michaelmalis2@gmail.com>
Discussion: https://www.postgresql.org/message-id/19595-7dc18b4e212c4757%40postgresql.org
---
src/backend/tsearch/Makefile | 4 +++-
.../tsearch/dicts/hunspell_test_afshort.affix | 5 +++++
.../tsearch/dicts/hunspell_test_afshort.dict | 1 +
.../tsearch/dicts/hunspell_test_aftrunc.affix | 5 +++++
.../tsearch/dicts/hunspell_test_aftrunc.dict | 1 +
src/backend/tsearch/spell.c | 18 +++++++++++++++++-
src/test/regress/expected/tsdicts.out | 16 ++++++++++++++++
src/test/regress/sql/tsdicts.sql | 16 ++++++++++++++++
8 files changed, 64 insertions(+), 2 deletions(-)
create mode 100644 src/backend/tsearch/dicts/hunspell_test_afshort.affix
create mode 100644 src/backend/tsearch/dicts/hunspell_test_afshort.dict
create mode 100644 src/backend/tsearch/dicts/hunspell_test_aftrunc.affix
create mode 100644 src/backend/tsearch/dicts/hunspell_test_aftrunc.dict
diff --git a/src/backend/tsearch/Makefile b/src/backend/tsearch/Makefile
index 4a436150109..9430d906122 100644
--- a/src/backend/tsearch/Makefile
+++ b/src/backend/tsearch/Makefile
@@ -18,7 +18,9 @@ DICTFILES=synonym_sample.syn thesaurus_sample.ths \
hunspell_sample.affix \
ispell_sample.affix ispell_sample.dict \
hunspell_sample_long.affix hunspell_sample_long.dict \
- hunspell_sample_num.affix hunspell_sample_num.dict
+ hunspell_sample_num.affix hunspell_sample_num.dict \
+ hunspell_test_afshort.affix hunspell_test_afshort.dict \
+ hunspell_test_aftrunc.affix hunspell_test_aftrunc.dict
# Local paths to dictionaries files
DICTFILES_PATH=$(addprefix dicts/,$(DICTFILES))
diff --git a/src/backend/tsearch/dicts/hunspell_test_afshort.affix b/src/backend/tsearch/dicts/hunspell_test_afshort.affix
new file mode 100644
index 00000000000..df0656b7b50
--- /dev/null
+++ b/src/backend/tsearch/dicts/hunspell_test_afshort.affix
@@ -0,0 +1,5 @@
+COMPOUNDFLAG Z
+AF 2
+AF x
+SFX A Y 1
+SFX A 0 0/2 .
diff --git a/src/backend/tsearch/dicts/hunspell_test_afshort.dict b/src/backend/tsearch/dicts/hunspell_test_afshort.dict
new file mode 100644
index 00000000000..8c5c4c3f000
--- /dev/null
+++ b/src/backend/tsearch/dicts/hunspell_test_afshort.dict
@@ -0,0 +1 @@
+foo/2
diff --git a/src/backend/tsearch/dicts/hunspell_test_aftrunc.affix b/src/backend/tsearch/dicts/hunspell_test_aftrunc.affix
new file mode 100644
index 00000000000..f1b6e5b9513
--- /dev/null
+++ b/src/backend/tsearch/dicts/hunspell_test_aftrunc.affix
@@ -0,0 +1,5 @@
+AF 2
+AF x
+
+SFX A Y 1
+SFX A 0 s .
diff --git a/src/backend/tsearch/dicts/hunspell_test_aftrunc.dict b/src/backend/tsearch/dicts/hunspell_test_aftrunc.dict
new file mode 100644
index 00000000000..257cc5642cb
--- /dev/null
+++ b/src/backend/tsearch/dicts/hunspell_test_aftrunc.dict
@@ -0,0 +1 @@
+foo
diff --git a/src/backend/tsearch/spell.c b/src/backend/tsearch/spell.c
index 3ded3cf7d5f..7323a5e15f2 100644
--- a/src/backend/tsearch/spell.c
+++ b/src/backend/tsearch/spell.c
@@ -1182,12 +1182,18 @@ getAffixFlagSet(IspellDict *Conf, char *s)
errmsg("invalid affix alias \"%s\"", s)));
if (curaffix > 0 && curaffix < Conf->nAffixData)
+ {
+ if (Conf->AffixData[curaffix] == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("invalid affix alias \"%s\"", s)));
/*
* Do not subtract 1 from curaffix because empty string was added
* in NIImportOOAffixes
*/
return Conf->AffixData[curaffix];
+ }
else if (curaffix > Conf->nAffixData)
ereport(ERROR,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
@@ -1422,6 +1428,13 @@ nextline:
tsearch_readline_end(&trst);
if (ptype)
pfree(ptype);
+
+ /* Reject incomplete AF alias table. */
+ if (Conf->useFlagAliases && curaffix != naffix)
+ ereport(ERROR,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("too few flag vector aliases (expected %d)",
+ naffix - 1)));
}
/*
@@ -1449,6 +1462,8 @@ NIImportAffixes(IspellDict *Conf, const char *filename)
bool oldformat = false;
char *recoded = NULL;
+ flag[0] = '\0'; /* no flag seen yet */
+
if (!tsearch_readline_begin(&trst, filename))
ereport(ERROR,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
@@ -1997,7 +2012,8 @@ NISortAffixes(IspellDict *Conf)
/* Store compound affixes in the Conf->CompoundAffix array */
if (Conf->naffixes > 1)
qsort(Conf->Affix, Conf->naffixes, sizeof(AFFIX), cmpaffix);
- Conf->CompoundAffix = ptr = palloc_array(CMPDAffix, Conf->naffixes);
+ /* +1 for terminator */
+ Conf->CompoundAffix = ptr = palloc_array(CMPDAffix, Conf->naffixes + 1);
ptr->affix = NULL;
for (int i = 0; i < Conf->naffixes; i++)
diff --git a/src/test/regress/expected/tsdicts.out b/src/test/regress/expected/tsdicts.out
index 0bbf2ff4ca2..fc56de6cc50 100644
--- a/src/test/regress/expected/tsdicts.out
+++ b/src/test/regress/expected/tsdicts.out
@@ -447,6 +447,22 @@ CREATE TEXT SEARCH DICTIONARY hunspell_err (
AffFile=hunspell_sample_long
);
ERROR: invalid affix alias "302,301,202,303"
+-- incomplete AF table (alias references an unfilled slot)
+\set VERBOSITY terse
+CREATE TEXT SEARCH DICTIONARY hunspell_test_afshort (
+ Template=ispell,
+ DictFile=hunspell_test_afshort,
+ AffFile=hunspell_test_afshort
+);
+ERROR: invalid affix alias "2"
+\set VERBOSITY default
+-- incomplete AF table (no reference to the missing slot)
+CREATE TEXT SEARCH DICTIONARY hunspell_test_aftrunc (
+ Template=ispell,
+ DictFile=hunspell_test_aftrunc,
+ AffFile=hunspell_test_aftrunc
+);
+ERROR: too few flag vector aliases (expected 2)
-- Synonym dictionary
CREATE TEXT SEARCH DICTIONARY synonym (
Template=synonym,
diff --git a/src/test/regress/sql/tsdicts.sql b/src/test/regress/sql/tsdicts.sql
index cf08410bb2d..9bb2cd1a848 100644
--- a/src/test/regress/sql/tsdicts.sql
+++ b/src/test/regress/sql/tsdicts.sql
@@ -138,6 +138,22 @@ CREATE TEXT SEARCH DICTIONARY hunspell_err (
AffFile=hunspell_sample_long
);
+-- incomplete AF table (alias references an unfilled slot)
+\set VERBOSITY terse
+CREATE TEXT SEARCH DICTIONARY hunspell_test_afshort (
+ Template=ispell,
+ DictFile=hunspell_test_afshort,
+ AffFile=hunspell_test_afshort
+);
+\set VERBOSITY default
+
+-- incomplete AF table (no reference to the missing slot)
+CREATE TEXT SEARCH DICTIONARY hunspell_test_aftrunc (
+ Template=ispell,
+ DictFile=hunspell_test_aftrunc,
+ AffFile=hunspell_test_aftrunc
+);
+
-- Synonym dictionary
CREATE TEXT SEARCH DICTIONARY synonym (
Template=synonym,
--
2.53.0
^ permalink raw reply [nested|flat] 15+ messages in thread
* Re: BUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3
@ 2026-08-02 17:25 Tom Lane <tgl@sss.pgh.pa.us>
parent: Andrey Rachitskiy <pl0h0yp1@gmail.com>
0 siblings, 2 replies; 15+ messages in thread
From: Tom Lane @ 2026-08-02 17:25 UTC (permalink / raw)
To: Andrey Rachitskiy <pl0h0yp1@gmail.com>; +Cc: michaelmalis2@gmail.com; pgsql-bugs@lists.postgresql.org
Andrey Rachitskiy <pl0h0yp1@gmail.com> writes:
> Hi, Michael!
> Patch attached.
I pushed these code changes with one minor tweak: adjusting the
new error message in NIImportOOAffixes to look more like the
existing one about too many aliases.
I left out the test cases. I don't think we need them, and
I certainly don't think we want to install intentionally-broken
files as sample data, as this patch would have done.
regards, tom lane
^ permalink raw reply [nested|flat] 15+ messages in thread
* Re: BUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3
@ 2026-08-02 17:42 Andrey Rachitskiy <pl0h0yp1@gmail.com>
parent: Tom Lane <tgl@sss.pgh.pa.us>
1 sibling, 0 replies; 15+ messages in thread
From: Andrey Rachitskiy @ 2026-08-02 17:42 UTC (permalink / raw)
To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: michaelmalis2@gmail.com; pgsql-bugs@lists.postgresql.org
Many thanks to Tom for merging the patch and for the review, to Michael for
the report, and I'm very glad to be of help!
вс, 2 авг. 2026 г. в 22:25, Tom Lane <tgl@sss.pgh.pa.us>:
> Andrey Rachitskiy <pl0h0yp1@gmail.com> writes:
> > Hi, Michael!
> > Patch attached.
>
> I pushed these code changes with one minor tweak: adjusting the
> new error message in NIImportOOAffixes to look more like the
> existing one about too many aliases.
>
> I left out the test cases. I don't think we need them, and
> I certainly don't think we want to install intentionally-broken
> files as sample data, as this patch would have done.
>
> regards, tom lane
>
--
Regards,
Rachitskiy Andrey
^ permalink raw reply [nested|flat] 15+ messages in thread
* Re: BUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3
@ 2026-08-02 20:00 Alexander Lakhin <exclusion@gmail.com>
parent: Tom Lane <tgl@sss.pgh.pa.us>
1 sibling, 1 reply; 15+ messages in thread
From: Alexander Lakhin @ 2026-08-02 20:00 UTC (permalink / raw)
To: Tom Lane <tgl@sss.pgh.pa.us>; Andrey Rachitskiy <pl0h0yp1@gmail.com>; +Cc: michaelmalis2@gmail.com; pgsql-bugs@lists.postgresql.org
Hello Tom,
02.08.2026 20:25, Tom Lane wrote:
> Andrey Rachitskiy<pl0h0yp1@gmail.com> writes:
>> Hi, Michael!
>> Patch attached.
> I pushed these code changes with one minor tweak: adjusting the
> new error message in NIImportOOAffixes to look more like the
> existing one about too many aliases.
>
> I left out the test cases. I don't think we need them, and
> I certainly don't think we want to install intentionally-broken
> files as sample data, as this patch would have done.
I'm not sure it's directly related to this bug report, but maybe you'd
like to fix one more memory-safety defect in tsearch in passing...
With the oom-simulation patch applied, the following script:
for i in {1..10}; do
echo "
SELECT COUNT(*) FROM pg_ts_dict;
CREATE TEXT SEARCH DICTIONARY thesaurus (Template=thesaurus, DictFile=thesaurus_sample, Dictionary=english_stem);
CREATE TEXT SEARCH CONFIGURATION tst (COPY=english);
SELECT to_tsvector('tst', 'Test test');
DROP TEXT SEARCH CONFIGURATION tst;
DROP TEXT SEARCH DICTIONARY thesaurus;
" | psql
grep 'was terminated' server.log && break;
done
fails for me as below:
2026-08-02 19:48:45.625 UTC [560023] LOG: client backend (PID 560036) was terminated by signal 11: Segmentation fault
Core was generated by `postgres: law regression [local] SELECT '.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x000055aecffd519e in MemoryContextSetIdentifier (context=0x7f7f7f7f7f7f7f7f, id=0x0) at mcxt.c:667
667 Assert(MemoryContextIsValid(context));
(gdb) bt
#0 0x000055aecffd519e in MemoryContextSetIdentifier (context=0x7f7f7f7f7f7f7f7f, id=0x0) at mcxt.c:667
#1 0x000055aecff863c1 in lookup_ts_dictionary_cache (dictId=13336) at ts_cache.c:307
#2 0x000055aecfd87f00 in LexizeExec (ld=0x7ffe75617c20, correspondLexem=0x0) at ts_parse.c:204
#3 0x000055aecfd88741 in parsetext (cfgId=16385, prs=0x7ffe75617cc0, buf=0x55aeee42bba4 "Test test~\177\1770",
buflen=9) at ts_parse.c:402
#4 0x000055aecfd8667d in to_tsvector_byid (fcinfo=0x55aeee5188c0) at to_tsany.c:260
#5 0x000055aecfa2a399 in ExecInterpExpr (state=0x55aeee5187e0, econtext=0x55aeee518d20, isnull=0x7ffe75618064) at
execExprInterp.c:1011
#6 0x000055aecfa2cf0b in ExecInterpExprStillValid (state=0x55aeee5187e0, econtext=0x55aeee518d20,
isNull=0x7ffe75618064) at execExprInterp.c:2309
#7 0x000055aecfbfba26 in ExecEvalExprSwitchContext (state=0x55aeee5187e0, econtext=0x55aeee518d20, isNull=0x7ffe75618064)
at ../../../../src/include/executor/executor.h:452
Plain `make check` triggers similar crashes as well...
Best regards,
Alexander
Attachments:
[text/x-patch] lookup_ts_dictionary_cache-oom.patch (2.4K, ../../0f3ddeb5-0dbd-479c-9d0e-ae254758e624@gmail.com/2-lookup_ts_dictionary_cache-oom.patch)
download | inline diff:
diff --git a/src/backend/utils/cache/ts_cache.c b/src/backend/utils/cache/ts_cache.c
index 9e29f1386b0..dbbba687aaf 100644
--- a/src/backend/utils/cache/ts_cache.c
+++ b/src/backend/utils/cache/ts_cache.c
@@ -291,10 +291,12 @@ lookup_ts_dictionary_cache(Oid dictId)
HASH_ENTER, &found);
Assert(!found); /* it wasn't there a moment ago */
+oom_prob = 0.5;
/* Create private memory context the first time through */
saveCtx = AllocSetContextCreate(CacheMemoryContext,
"TS dictionary",
ALLOCSET_SMALL_SIZES);
+oom_prob = 0;
MemoryContextCopyAndSetIdentifier(saveCtx, NameStr(dict->dictname));
}
else
diff --git a/src/backend/utils/mmgr/aset.c b/src/backend/utils/mmgr/aset.c
index 6a9ea367107..5883ec7fb99 100644
--- a/src/backend/utils/mmgr/aset.c
+++ b/src/backend/utils/mmgr/aset.c
@@ -51,6 +51,7 @@
#include "utils/memutils.h"
#include "utils/memutils_internal.h"
#include "utils/memutils_memorychunk.h"
+#include "common/pg_prng.h"
/*--------------------
* Chunk freelist k holds chunks of size 1 << (k + ALLOC_MINBITS),
@@ -441,7 +442,7 @@ AllocSetContextCreateInternal(MemoryContext parent,
* Allocate the initial block. Unlike other aset.c blocks, it starts with
* the context header and its block header follows that.
*/
- set = (AllocSet) malloc(firstBlockSize);
+ set = (pg_prng_double(&pg_global_prng_state) < oom_prob) ? NULL : (AllocSet) malloc(firstBlockSize);
if (set == NULL)
{
if (TopMemoryContext)
diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c
index 930fc457328..af050ed870d 100644
--- a/src/backend/utils/mmgr/mcxt.c
+++ b/src/backend/utils/mmgr/mcxt.c
@@ -46,6 +46,7 @@
#include "utils/memutils_internal.h"
#include "utils/memutils_memorychunk.h"
+double oom_prob = 0;
static void BogusFree(void *pointer);
static void *BogusRealloc(void *pointer, Size size, int flags);
diff --git a/src/include/utils/palloc.h b/src/include/utils/palloc.h
index 0e934158b60..90033ddc5a3 100644
--- a/src/include/utils/palloc.h
+++ b/src/include/utils/palloc.h
@@ -163,5 +163,6 @@ extern char *pchomp(const char *in);
/* sprintf into a palloc'd buffer --- these are in psprintf.c */
extern char *psprintf(const char *fmt, ...) pg_attribute_printf(1, 2);
extern size_t pvsnprintf(char *buf, size_t len, const char *fmt, va_list args) pg_attribute_printf(3, 0);
+extern double oom_prob;
#endif /* PALLOC_H */
^ permalink raw reply [nested|flat] 15+ messages in thread
* Re: BUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3
@ 2026-08-02 20:11 Tom Lane <tgl@sss.pgh.pa.us>
parent: Alexander Lakhin <exclusion@gmail.com>
0 siblings, 2 replies; 15+ messages in thread
From: Tom Lane @ 2026-08-02 20:11 UTC (permalink / raw)
To: Alexander Lakhin <exclusion@gmail.com>; +Cc: Andrey Rachitskiy <pl0h0yp1@gmail.com>; michaelmalis2@gmail.com; pgsql-bugs@lists.postgresql.org
Alexander Lakhin <exclusion@gmail.com> writes:
> I'm not sure it's directly related to this bug report, but maybe you'd
> like to fix one more memory-safety defect in tsearch in passing...
Hmph. Not sure I'd call that "memory safety", but yeah, this bit
isn't being careful about having a valid intermediate state of the
data structure. Thanks for the report!
regards, tom lane
^ permalink raw reply [nested|flat] 15+ messages in thread
* Re: BUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3
@ 2026-08-24 11:20 Ewan Young <kdbase.hack@gmail.com>
parent: Tom Lane <tgl@sss.pgh.pa.us>
1 sibling, 1 reply; 15+ messages in thread
From: Ewan Young @ 2026-08-24 11:20 UTC (permalink / raw)
To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Alexander Lakhin <exclusion@gmail.com>; Andrey Rachitskiy <pl0h0yp1@gmail.com>; michaelmalis2@gmail.com; pgsql-bugs@lists.postgresql.org
Hi,
One more problem in the same file. It is not one of the three in the
original report - those were all on the affix-rule side (CompoundAffix,
the flag buffer, the AF alias table), while this one is in the compound
flag table - and it is older than all of them, the code being from 9.6.
So I'm posting here rather than opening a new report.
CompoundAffixFlag holds a flag in a union whose member is chosen by the
flag mode the affix file's FLAG line declares, and NIImportOOAffixes()
converts each COMPOUNDFLAG / ONLYINCOMPOUND / ... flag as soon as it reads
the line, using the mode in effect at that point. FLAG may appear anywhere
in the file, so flags read before and after it can disagree about which
member holds the value. cmpcmdflag() takes the mode from its first
argument alone and applies it to both, so it can read an integer as a char
pointer and hand it to strcmp().
Three ways this shows up. Starting with the one that doesn't crash, which
I think matters most: take the shipped hunspell_sample_num dictionary and
move its FLAG line below the compound options, changing nothing else.
COMPOUNDFLAG 101
ONLYINCOMPOUND 102
FLAG num
SELECT ts_lexize('withflagfirst', 'footballklubber');
{footballklubber,foot,ball,klubber,football,klubber}
SELECT ts_lexize('withflaglast', 'footballklubber');
{footballklubber}
The entries are stored as strings while the lookup key is built with the
final mode, so the numeric comparison compares a pointer's low half against
a number, never matches, and the compound flag is never found. No error,
no warning, and suffix handling still works ('books' -> {book}), so the
dictionary looks healthy. Compound splitting is simply gone, along with
every lexeme it would have produced: a search for 'ball' stops finding the
document, and a GIN index built this way never contained those lexemes.
The two crashing shapes:
COMPOUNDFLAG A / FLAG num / COMPOUNDBEGIN 1
-> heterogeneous array, dies in the sort
SIGSEGV __strcmp_evex <- cmpcmdflag spell.c:226 <- pg_qsort
<- NIImportOOAffixes spell.c:1306
FLAG num / COMPOUNDFLAG 101 / ONLYINCOMPOUND 102 / FLAG long
-> array consistent, but the lookup key uses the final mode
SIGSEGV <- cmpcmdflag spell.c:226 <- bsearch
<- getCompoundAffixFlagValue spell.c:1152
<- makeCompoundFlags spell.c:1652 <- mkSPNode spell.c:1725
With --enable-cassert the Assert() in cmpcmdflag() fires first. All three
reproduce on 8646214ee20 on both build types, and the code involved has not
changed in a long time. Reachability is as for the defects fixed by
330a72052cd - you must be able to place a file in $SHAREDIR/tsearch_data -
and every dictionary I know of, including all four shipped samples, puts
FLAG first, which is presumably why this went unreported for ten years.
0001 keeps the flags as strings while the file is read and converts them
once it has been read in full, when the mode is final. That removes all
three symptoms, since the array and the lookup key then use the same
representation.
It also makes the position of FLAG irrelevant, which is what the rest of
the parser already does: AF, SFX and PFX flags are parsed in a second pass
and so always use the final mode. PG is in fact already more permissive
than hunspell(5), which says "If the affix file contains the FLAG parameter,
define it before the AF definitions" - move FLAG below the AF lines of
hunspell_sample_long and PG still gets it right. So FLAG is currently
file-scoped for AF/SFX/PFX and line-scoped for the eight compound options;
0001 makes the latter agree. Erroring out instead would be about six
lines, but that would leave us stricter than Hunspell for compound options
while staying looser for AF, which seems hard to justify.
Note the old ispell format reaches this code from NIImportAffixes() and
returns without entering NIImportOOAffixes(), so it needs the conversion
too - I missed that at first and ispell_sample promptly crashed the
regression run.
0002 is optional cleanup. With 0001 in place every entry agrees
with Conf->flagMode, so the per-entry copy of the mode is redundant; it
exists only because of the comment on the field, "we don't have a
bsearch_arg version, so, copy FlagMode", which stopped being true when
bsearch_arg() moved to src/port in bfa2cee7841. Taking the mode through
qsort_arg()/bsearch_arg() lets the field and the Assert() go. No functional
change, and 0001 does not depend on it.
Regards,
Ewan Young
On Mon, Aug 3, 2026 at 4:11 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
>
> Alexander Lakhin <exclusion@gmail.com> writes:
> > I'm not sure it's directly related to this bug report, but maybe you'd
> > like to fix one more memory-safety defect in tsearch in passing...
>
> Hmph. Not sure I'd call that "memory safety", but yeah, this bit
> isn't being careful about having a valid intermediate state of the
> data structure. Thanks for the report!
>
> regards, tom lane
>
>
--
Regards,
Ewan Young
Attachments:
[application/octet-stream] v1-0001-Don-t-convert-Hunspell-compound-flags-before-the-fla.patch (6.3K, ../../CAON2xHN3QmsaySM6DGWa1gttcbJoFh0wjAE-_ZpSPo=LKN1hYw@mail.gmail.com/2-v1-0001-Don-t-convert-Hunspell-compound-flags-before-the-fla.patch)
download | inline diff:
From c5db6fafc018a22bbdc9eea0b057b09d6f92ea3d Mon Sep 17 00:00:00 2001
From: Ewan Young <kdbase.hack@gmail.com>
Date: Tue, 25 Aug 2026 01:56:40 +0800
Subject: [PATCH v1 1/2] Don't convert Hunspell compound flags before the flag
mode is known
The compound flags collected from COMPOUNDFLAG and friends are stored in
either the string or the integer member of a union, chosen by 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. cmpcmdflag() takes the mode from its first argument alone 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.
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.
Note that the old ispell file format reaches addCompoundAffixFlagValue()
too, from NIImportAffixes(), and returns without entering
NIImportOOAffixes(), so it needs the conversion step as well.
---
src/backend/tsearch/spell.c | 99 ++++++++++++++++++++++++++++++-------
1 file changed, 80 insertions(+), 19 deletions(-)
diff --git a/src/backend/tsearch/spell.c b/src/backend/tsearch/spell.c
index ceea8ead65a..0e2b4f91390 100644
--- a/src/backend/tsearch/spell.c
+++ b/src/backend/tsearch/spell.c
@@ -1033,31 +1033,41 @@ parse_affentry(const char *str, char *mask, char *find, char *repl)
return (*mask && (*find || *repl));
}
+/*
+ * Parse an affix flag written in the "num" flag mode.
+ */
+static uint32
+parseNumericAffixFlag(const char *s)
+{
+ 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)));
+
+ return 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);
@@ -1120,12 +1130,54 @@ 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.
@@ -1302,6 +1354,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(Conf->CompoundAffixFlags, Conf->nCompoundAffixFlag,
sizeof(CompoundAffixFlag), cmpcmdflag);
@@ -1576,6 +1631,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:
--
2.47.3
[application/octet-stream] v1-0002-Drop-the-per-entry-copy-of-the-flag-mode-in-Compound.patch (4.3K, ../../CAON2xHN3QmsaySM6DGWa1gttcbJoFh0wjAE-_ZpSPo=LKN1hYw@mail.gmail.com/3-v1-0002-Drop-the-per-entry-copy-of-the-flag-mode-in-Compound.patch)
download | inline diff:
From 1a6c6b0265b9cff4ca38baa672dd825e2ab7bde1 Mon Sep 17 00:00:00 2001
From: Ewan Young <kdbase.hack@gmail.com>
Date: Tue, 25 Aug 2026 02:51:54 +0800
Subject: [PATCH v1 2/2] Drop the per-entry copy of the flag mode in
CompoundAffixFlags
Each CompoundAffixFlag carried its own copy of the dictionary's flag
mode, which decides whether the union holds a string or an integer. The
comment on the field explained why: cmpcmdflag() needs the mode, and at
the time there was no bsearch() variant that could be passed a context
pointer. bsearch_arg() has existed since bfa2cee7841, so the copies can
go away and the mode can be taken from the dictionary itself, where it
belongs. Since every entry necessarily agreed with Conf->flagMode once
the flags are converted in one place, the copies were pure redundancy,
and the Assert() that checked they agreed can go as well.
No functional change. sizeof(CompoundAffixFlag) is unchanged, the struct
is used only while a dictionary is being built, and it is private to
spell.c and its header.
---
src/backend/tsearch/spell.c | 22 +++++++++-------------
src/include/tsearch/dicts/spell.h | 6 ++----
2 files changed, 11 insertions(+), 17 deletions(-)
diff --git a/src/backend/tsearch/spell.c b/src/backend/tsearch/spell.c
index 0e2b4f91390..3a922dfa22b 100644
--- a/src/backend/tsearch/spell.c
+++ b/src/backend/tsearch/spell.c
@@ -208,14 +208,13 @@ cmpspellaffix(const void *s1, const void *s2)
}
static int
-cmpcmdflag(const void *f1, const void *f2)
+cmpcmdflag(const void *f1, const void *f2, void *arg)
{
const CompoundAffixFlag *fv1 = f1;
const CompoundAffixFlag *fv2 = f2;
+ FlagMode flagMode = *(const FlagMode *) arg;
- Assert(fv1->flagMode == fv2->flagMode);
-
- if (fv1->flagMode == FM_NUM)
+ if (flagMode == FM_NUM)
{
if (fv1->flag.i == fv2->flag.i)
return 0;
@@ -1071,7 +1070,6 @@ setCompoundAffixFlagValue(IspellDict *Conf, CompoundAffixFlag *entry,
else
entry->flag.s = cpstrdup(Conf, s);
- entry->flagMode = Conf->flagMode;
entry->value = val;
}
@@ -1135,7 +1133,7 @@ addCompoundAffixFlagValue(IspellDict *Conf, const char *s, uint32 val)
* 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.
+ * finalizeCompoundAffixFlags().
*
* The interim copy goes in the short-lived build context, since the final
* representation may well not be a string at all.
@@ -1173,8 +1171,6 @@ finalizeCompoundAffixFlags(IspellDict *Conf)
entry->flag.i = parseNumericAffixFlag(entry->flag.s);
else
entry->flag.s = cpstrdup(Conf, entry->flag.s);
-
- entry->flagMode = Conf->flagMode;
}
}
@@ -1201,9 +1197,9 @@ getCompoundAffixFlagValue(IspellDict *Conf, const char *s)
setCompoundAffixFlagValue(Conf, &key, sflag, 0);
found = (CompoundAffixFlag *)
- bsearch(&key, Conf->CompoundAffixFlags,
- Conf->nCompoundAffixFlag, sizeof(CompoundAffixFlag),
- cmpcmdflag);
+ bsearch_arg(&key, Conf->CompoundAffixFlags,
+ Conf->nCompoundAffixFlag, sizeof(CompoundAffixFlag),
+ cmpcmdflag, &Conf->flagMode);
if (found != NULL)
flag |= found->value;
}
@@ -1358,8 +1354,8 @@ NIImportOOAffixes(IspellDict *Conf, const char *filename)
finalizeCompoundAffixFlags(Conf);
if (Conf->nCompoundAffixFlag > 1)
- qsort(Conf->CompoundAffixFlags, Conf->nCompoundAffixFlag,
- sizeof(CompoundAffixFlag), cmpcmdflag);
+ qsort_arg(Conf->CompoundAffixFlags, Conf->nCompoundAffixFlag,
+ sizeof(CompoundAffixFlag), cmpcmdflag, &Conf->flagMode);
if (!tsearch_readline_begin(&trst, filename))
ereport(ERROR,
diff --git a/src/include/tsearch/dicts/spell.h b/src/include/tsearch/dicts/spell.h
index 038b4384fb4..92ae1f35c59 100644
--- a/src/include/tsearch/dicts/spell.h
+++ b/src/include/tsearch/dicts/spell.h
@@ -169,13 +169,11 @@ typedef struct CompoundAffixFlag
{
union
{
- /* Flag name if flagMode is FM_CHAR or FM_LONG */
+ /* Flag name if the dictionary's flagMode is FM_CHAR or FM_LONG */
const char *s;
- /* Flag name if flagMode is FM_NUM */
+ /* Flag name if the dictionary's flagMode is FM_NUM */
uint32 i;
} flag;
- /* we don't have a bsearch_arg version, so, copy FlagMode */
- FlagMode flagMode;
uint32 value;
} CompoundAffixFlag;
--
2.47.3
^ permalink raw reply [nested|flat] 15+ messages in thread
* Re: BUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3
@ 2026-08-30 00:14 Tom Lane <tgl@sss.pgh.pa.us>
parent: Ewan Young <kdbase.hack@gmail.com>
0 siblings, 0 replies; 15+ messages in thread
From: Tom Lane @ 2026-08-30 00:14 UTC (permalink / raw)
To: Ewan Young <kdbase.hack@gmail.com>; +Cc: Alexander Lakhin <exclusion@gmail.com>; Andrey Rachitskiy <pl0h0yp1@gmail.com>; michaelmalis2@gmail.com; pgsql-bugs@lists.postgresql.org
Ewan Young <kdbase.hack@gmail.com> writes:
> One more problem in the same file. It is not one of the three in the
> original report - those were all on the affix-rule side (CompoundAffix,
> the flag buffer, the AF alias table), while this one is in the compound
> flag table - and it is older than all of them, the code being from 9.6.
> So I'm posting here rather than opening a new report.
Pushed, thanks for the report!
I noticed while reading your patch that all of the strtol() calls in
this file store the result into an "int" not a "long", which opens
the door to an undetected integer overflow and truncation. Nothing
terribly harmful seems likely to ensue, but I thought I'd clean that
up too while we're here.
regards, tom lane
^ permalink raw reply [nested|flat] 15+ messages in thread
* Re: BUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3
@ 2026-08-30 04:00 Alexander Lakhin <exclusion@gmail.com>
parent: Tom Lane <tgl@sss.pgh.pa.us>
1 sibling, 1 reply; 15+ messages in thread
From: Alexander Lakhin @ 2026-08-30 04:00 UTC (permalink / raw)
To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Andrey Rachitskiy <pl0h0yp1@gmail.com>; michaelmalis2@gmail.com; pgsql-bugs@lists.postgresql.org
Hello Tom,
02.08.2026 23:11, Tom Lane wrote:
> Hmph. Not sure I'd call that "memory safety", but yeah, this bit
> isn't being careful about having a valid intermediate state of the
> data structure. Thanks for the report!
I discovered one more issue in this area. This OOM condition emulation:
--- a/src/backend/snowball/libstemmer/api.c
+++ b/src/backend/snowball/libstemmer/api.c
@@ -6,3 +6,3 @@ extern struct SN_env * SN_new_env(int alloc_size)
{
- struct SN_env * z = (struct SN_env *) malloc(alloc_size);
+ struct SN_env * z = (rand() % 2 == 0) ? NULL : (struct SN_env *) malloc(alloc_size);
if (z == NULL) return NULL;
leads to `make check` crashes like:
2026-08-30 06:42:24.759 EEST postmaster[1423557] LOG: client backend (PID 1423766) was terminated by signal 11:
Segmentation fault
2026-08-30 06:42:24.759 EEST postmaster[1423557] DETAIL: Failed process was running: SELECT
ts_delete(to_tsvector('english', 'Rebel spaceships, striking from a hidden base'), 'spaceship')
or
2026-08-30 06:46:50.112 EEST postmaster[1426347] LOG: client backend (PID 1427391) was terminated by signal 11:
Segmentation fault
2026-08-30 06:46:50.112 EEST postmaster[1426347] DETAIL: Failed process was running: SELECT ts_lexize('thesaurus', 'one');
Could you please have a look if you're still around?
Best regards,
Alexander
^ permalink raw reply [nested|flat] 15+ messages in thread
* Re: BUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3
@ 2026-08-30 04:04 Tom Lane <tgl@sss.pgh.pa.us>
parent: Alexander Lakhin <exclusion@gmail.com>
0 siblings, 1 reply; 15+ messages in thread
From: Tom Lane @ 2026-08-30 04:04 UTC (permalink / raw)
To: Alexander Lakhin <exclusion@gmail.com>; +Cc: Andrey Rachitskiy <pl0h0yp1@gmail.com>; michaelmalis2@gmail.com; pgsql-bugs@lists.postgresql.org
Alexander Lakhin <exclusion@gmail.com> writes:
> I discovered one more issue in this area. This OOM condition emulation:
> --- a/src/backend/snowball/libstemmer/api.c
> +++ b/src/backend/snowball/libstemmer/api.c
> @@ -6,3 +6,3 @@ extern struct SN_env * SN_new_env(int alloc_size)
> {
> - struct SN_env * z = (struct SN_env *) malloc(alloc_size);
> + struct SN_env * z = (rand() % 2 == 0) ? NULL : (struct SN_env *) malloc(alloc_size);
> if (z == NULL) return NULL;
> leads to `make check` crashes like:
Hmph. SN_new_env itself is visibly okay with this, so the failure is
in some caller. I'm too tired to dig into it myself, but can you
identify the culprit more precisely?
regards, tom lane
^ permalink raw reply [nested|flat] 15+ messages in thread
* Re: BUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3
@ 2026-08-30 04:30 Alexander Lakhin <exclusion@gmail.com>
parent: Tom Lane <tgl@sss.pgh.pa.us>
0 siblings, 1 reply; 15+ messages in thread
From: Alexander Lakhin @ 2026-08-30 04:30 UTC (permalink / raw)
To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Andrey Rachitskiy <pl0h0yp1@gmail.com>; michaelmalis2@gmail.com; pgsql-bugs@lists.postgresql.org
30.08.2026 07:04, Tom Lane wrote:
> Hmph. SN_new_env itself is visibly okay with this, so the failure is
> in some caller. I'm too tired to dig into it myself, but can you
> identify the culprit more precisely?
I think it's:
static void
locate_stem_module(DictSnowball *d, const char *lang)
{
...
d->stem = m->stem;
d->z = m->create();
...
(in two places)
then d->z is dereferenced in dsnowball_lexize().
Thank you for your time!
Best regards,
Alexander
^ permalink raw reply [nested|flat] 15+ messages in thread
* Re: BUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3
@ 2026-08-30 04:37 Andrey Rachitskiy <pl0h0yp1@gmail.com>
parent: Alexander Lakhin <exclusion@gmail.com>
0 siblings, 1 reply; 15+ messages in thread
From: Andrey Rachitskiy @ 2026-08-30 04:37 UTC (permalink / raw)
To: Alexander Lakhin <exclusion@gmail.com>; +Cc: Tom Lane <tgl@sss.pgh.pa.us>; michaelmalis2@gmail.com; pgsql-bugs@lists.postgresql.org
вс, 30 авг. 2026 г. в 09:30, Alexander Lakhin <exclusion@gmail.com>:
> 30.08.2026 07:04, Tom Lane wrote:
>
> Hmph. SN_new_env itself is visibly okay with this, so the failure is
> in some caller. I'm too tired to dig into it myself, but can you
> identify the culprit more precisely?
>
>
> I think it's:
> static void
> locate_stem_module(DictSnowball *d, const char *lang)
> {
> ...
> d->stem = m->stem;
> d->z = m->create();
> ...
> (in two places)
>
> then d->z is dereferenced in dsnowball_lexize().
>
> Confirm.
locate_stem_module() in dict_snowball.c stores m->create() into d->z
without checking. *_create_env wrappers correctly return NULL when
SN_new_env fails. dsnowball_init then succeeds with d->z == NULL, and
dsnowball_lexize crashes in SN_set_current / replace_s.
--
Regards,
Rachitskiy Andrey
^ permalink raw reply [nested|flat] 15+ messages in thread
* Re: BUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3
@ 2026-08-30 04:43 Andrey Rachitskiy <pl0h0yp1@gmail.com>
parent: Andrey Rachitskiy <pl0h0yp1@gmail.com>
0 siblings, 1 reply; 15+ messages in thread
From: Andrey Rachitskiy @ 2026-08-30 04:43 UTC (permalink / raw)
To: Alexander Lakhin <exclusion@gmail.com>; +Cc: Tom Lane <tgl@sss.pgh.pa.us>; michaelmalis2@gmail.com; pgsql-bugs@lists.postgresql.org
вс, 30 авг. 2026 г. в 09:37, Andrey Rachitskiy <pl0h0yp1@gmail.com>:
>
>
> вс, 30 авг. 2026 г. в 09:30, Alexander Lakhin <exclusion@gmail.com>:
>
>> 30.08.2026 07:04, Tom Lane wrote:
>>
>> Hmph. SN_new_env itself is visibly okay with this, so the failure is
>> in some caller. I'm too tired to dig into it myself, but can you
>> identify the culprit more precisely?
>>
>>
>> I think it's:
>> static void
>> locate_stem_module(DictSnowball *d, const char *lang)
>> {
>> ...
>> d->stem = m->stem;
>> d->z = m->create();
>> ...
>> (in two places)
>>
>> then d->z is dereferenced in dsnowball_lexize().
>>
>> Confirm.
> locate_stem_module() in dict_snowball.c stores m->create() into d->z
> without checking. *_create_env wrappers correctly return NULL when
> SN_new_env fails. dsnowball_init then succeeds with d->z == NULL, and
> dsnowball_lexize crashes in SN_set_current / replace_s.
>
> --
> Regards,
> Rachitskiy Andrey
>
Fix in attach
Attachments:
[text/x-patch] 0001-Check-for-NULL-from-Snowball-create_env-in-dsnowball.patch (1.3K, ../../CAB8bMit+ARmO25cBKJRkFcGK1Dwe54CvgUc0yOHwgnvSD6iO2w@mail.gmail.com/3-0001-Check-for-NULL-from-Snowball-create_env-in-dsnowball.patch)
download | inline diff:
From 83ac9194fb041d80f93a4bf1a852abc9b1471247 Mon Sep 17 00:00:00 2001
From: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Date: Sun, 30 Aug 2026 09:28:11 +0500
Subject: [PATCH] Check for NULL from Snowball create_env in dsnowball_init.
SN_new_env and the per-language create_env wrappers return NULL on
allocation failure, but locate_stem_module stored that NULL and let
dsnowball_init succeed. Later SN_set_current in dsnowball_lexize then
crashed. Reject a NULL env after the language module is selected.
Author: Andrey Rachitskiy <pl0h0yp1@gmail.com>
Reported-by: Alexander Lakhin <exclusion@gmail.com>
---
src/backend/snowball/dict_snowball.c | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/backend/snowball/dict_snowball.c b/src/backend/snowball/dict_snowball.c
index 182bd156995..1213a59323c 100644
--- a/src/backend/snowball/dict_snowball.c
+++ b/src/backend/snowball/dict_snowball.c
@@ -276,6 +276,14 @@ dsnowball_init(PG_FUNCTION_ARGS)
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("missing Language parameter")));
+ /*
+ * Snowball create_env returns NULL on allocation failure. Catch it here
+ */
+ if (d->z == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_OUT_OF_MEMORY),
+ errmsg("out of memory")));
+
d->dictCtx = CurrentMemoryContext;
PG_RETURN_POINTER(d);
--
2.53.0
^ permalink raw reply [nested|flat] 15+ messages in thread
* Re: BUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3
@ 2026-08-30 11:43 Andrey Rachitskiy <pl0h0yp1@gmail.com>
parent: Andrey Rachitskiy <pl0h0yp1@gmail.com>
0 siblings, 1 reply; 15+ messages in thread
From: Andrey Rachitskiy @ 2026-08-30 11:43 UTC (permalink / raw)
To: Alexander Lakhin <exclusion@gmail.com>; +Cc: Tom Lane <tgl@sss.pgh.pa.us>; michaelmalis2@gmail.com; pgsql-bugs@lists.postgresql.org
вс, 30 авг. 2026 г. в 09:43, Andrey Rachitskiy <pl0h0yp1@gmail.com>:
>
> Fix in attach
>
Looking again at how Snowball is wired in the backend, I was too
quick with the d->z NULL check.
In the backend, snowball_runtime.h remaps malloc to palloc
(src/include/snowball/snowball_runtime.h). api.c includes that
header via the -I order in the snowball Makefile / meson.build, so
SN_new_env()'s malloc is palloc. On allocation failure palloc does
not return NULL. It goes through MemoryContextAllocationFailure().
So the live OOM path never reaches SN_new_env()'s
"if (z == NULL) return NULL" (Or what don't I know?) , never leaves d->z
NULL, and never reaches the SEGV in dsnowball_lexize.
Alexander's change is different. It forces a NULL before the
(remapped) malloc runs:
z = (rand() % 2 == 0) ? NULL : malloc(alloc_size);
That is the Snowball C API's "malloc failed".
The patch I sent only covers that emulated NULL-return case.
It does not change behaviour for a real out-of-memory under palloc.
Sorry for the noise :).
--
Regards,
Rachitskiy Andrey
^ permalink raw reply [nested|flat] 15+ messages in thread
* Re: BUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3
@ 2026-08-30 15:14 Tom Lane <tgl@sss.pgh.pa.us>
parent: Andrey Rachitskiy <pl0h0yp1@gmail.com>
0 siblings, 0 replies; 15+ messages in thread
From: Tom Lane @ 2026-08-30 15:14 UTC (permalink / raw)
To: Andrey Rachitskiy <pl0h0yp1@gmail.com>; +Cc: Alexander Lakhin <exclusion@gmail.com>; michaelmalis2@gmail.com; pgsql-bugs@lists.postgresql.org
Andrey Rachitskiy <pl0h0yp1@gmail.com> writes:
> In the backend, snowball_runtime.h remaps malloc to palloc
> (src/include/snowball/snowball_runtime.h). api.c includes that
> header via the -I order in the snowball Makefile / meson.build, so
> SN_new_env()'s malloc is palloc. On allocation failure palloc does
> not return NULL. It goes through MemoryContextAllocationFailure().
Ah, right. You can confirm that SN_new_env is really using palloc:
$ nm --ext --undef api.o | grep alloc
U palloc
It's like this to prevent memory leaks while not modifying the
machine-generated Snowball .c files, but I concede it's confusing.
Anyway it looks like we have nothing to do here. The Snowball code
is correct on its own terms to defend against null results, but our
calling code is equally correct to not worry about that.
regards, tom lane
^ permalink raw reply [nested|flat] 15+ messages in thread
end of thread, other threads:[~2026-08-30 15:14 UTC | newest]
Thread overview: 15+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-08-02 01:10 BUG #19595: Three memory-safety defects in src/backend/tsearch/spell.c (dictionary loader), PG 18.3 PG Bug reporting form <noreply@postgresql.org>
2026-08-02 07:29 ` Andrey Rachitskiy <pl0h0yp1@gmail.com>
2026-08-02 17:25 ` Tom Lane <tgl@sss.pgh.pa.us>
2026-08-02 17:42 ` Andrey Rachitskiy <pl0h0yp1@gmail.com>
2026-08-02 20:00 ` Alexander Lakhin <exclusion@gmail.com>
2026-08-02 20:11 ` Tom Lane <tgl@sss.pgh.pa.us>
2026-08-24 11:20 ` Ewan Young <kdbase.hack@gmail.com>
2026-08-30 00:14 ` Tom Lane <tgl@sss.pgh.pa.us>
2026-08-30 04:00 ` Alexander Lakhin <exclusion@gmail.com>
2026-08-30 04:04 ` Tom Lane <tgl@sss.pgh.pa.us>
2026-08-30 04:30 ` Alexander Lakhin <exclusion@gmail.com>
2026-08-30 04:37 ` Andrey Rachitskiy <pl0h0yp1@gmail.com>
2026-08-30 04:43 ` Andrey Rachitskiy <pl0h0yp1@gmail.com>
2026-08-30 11:43 ` Andrey Rachitskiy <pl0h0yp1@gmail.com>
2026-08-30 15:14 ` Tom Lane <tgl@sss.pgh.pa.us>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox