1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-23 13:29:09 +03:00

Avoid overflow in Levenshtein distance calculations.

levenshtein() and levenshtein_less_equal() let the caller specify
the insertion, deletion, and substitution costs, and
fuzzystrmatch's corresponding SQL functions accept any 32-bit
integer for each.  Since the distances are calculated with 32-bit
arithmetic, large costs can cause overflows, thereby producing
nonsensical results.  Certain inputs to levenshtein_less_equal()
can even cause out-of-bounds writes.  To fix, use 64-bit arithmetic
instead, and error whenever the final result won't fit in the
returned 32-bit integer.

We may want to teach these functions to reject negative costs, too,
but that didn't seem appropriate for a security fix, and therefore
it is left as a future exercise.

Reported-by: Ben Morris in collaboration with Claude and Anthropic Research
Author: Nathan Bossart <nathandbossart@gmail.com>
Reviewed-by: Dean Rasheed <dean.a.rasheed@gmail.com>
Security: CVE-2026-15742
Backpatch-through: 14
This commit is contained in:
Nathan Bossart
2026-08-10 06:38:24 -07:00
committed by Noah Misch
parent 7d150b5c94
commit 61481aa0e4
4 changed files with 77 additions and 43 deletions

View File

@@ -41,6 +41,14 @@ SELECT levenshtein('GUMBO', 'GAMBOL', 2, 1, 1);
3
(1 row)
SELECT levenshtein('GUMBO', 'GAMBOL', 1, 1, 2000000000);
levenshtein
-------------
3
(1 row)
SELECT levenshtein('GUMBO', 'GAMBOL', 2000000000, 2000000000, 2000000000);
ERROR: levenshtein distance out of range
SELECT levenshtein_less_equal('extensive', 'exhaustive', 2);
levenshtein_less_equal
------------------------
@@ -53,6 +61,12 @@ SELECT levenshtein_less_equal('extensive', 'exhaustive', 4);
4
(1 row)
SELECT levenshtein_less_equal('aaa', 'aaaaa', 1073741824, 0, 1073741824, 10);
levenshtein_less_equal
------------------------
11
(1 row)
SELECT metaphone('GUMBO', 4);
metaphone
-----------

View File

@@ -11,8 +11,11 @@ SELECT soundex(''), difference('', '');
SELECT levenshtein('GUMBO', 'GAMBOL');
SELECT levenshtein('GUMBO', 'GAMBOL', 2, 1, 1);
SELECT levenshtein('GUMBO', 'GAMBOL', 1, 1, 2000000000);
SELECT levenshtein('GUMBO', 'GAMBOL', 2000000000, 2000000000, 2000000000);
SELECT levenshtein_less_equal('extensive', 'exhaustive', 2);
SELECT levenshtein_less_equal('extensive', 'exhaustive', 4);
SELECT levenshtein_less_equal('aaa', 'aaaaa', 1073741824, 0, 1073741824, 10);
SELECT metaphone('GUMBO', 4);

View File

@@ -9,7 +9,7 @@
* Levenshtein distance with custom costings, and (2) Levenshtein distance with
* custom costings and a "max" value above which exact distances are not
* interesting. Before the inclusion, we rely on the presence of the inline
* function rest_of_char_same().
* functions rest_of_char_same() and levenshtein_result().
*
* Written based on a description of the algorithm by Michael Gilleland found
* at http://www.merriampark.com/ld.htm. Also looked at levenshtein.c in the
@@ -78,13 +78,16 @@ varstr_levenshtein(const char *source, int slen,
{
int m,
n;
int *prev;
int *curr;
int64 *prev;
int64 *curr;
int *s_char_len = NULL;
int j;
const char *y;
const char *send = source + slen;
const char *tend = target + tlen;
int64 ins_c_64 = ins_c;
int64 del_c_64 = del_c;
int64 sub_c_64 = sub_c;
/*
* For varstr_levenshtein_less_equal, we have real variables called
@@ -115,9 +118,9 @@ varstr_levenshtein(const char *source, int slen,
* into an empty s with m deletions.
*/
if (!m)
return n * ins_c;
return levenshtein_result(n * ins_c_64);
if (!n)
return m * del_c;
return levenshtein_result(m * del_c_64);
/*
* For security concerns, restrict excessive CPU+RAM usage. (This
@@ -147,20 +150,20 @@ varstr_levenshtein(const char *source, int slen,
*/
if (max_d >= 0)
{
int min_theo_d; /* Theoretical minimum distance. */
int max_theo_d; /* Theoretical maximum distance. */
int64 min_theo_d; /* Theoretical minimum distance. */
int64 max_theo_d; /* Theoretical maximum distance. */
int net_inserts = n - m;
min_theo_d = net_inserts < 0 ?
-net_inserts * del_c : net_inserts * ins_c;
-net_inserts * del_c_64 : net_inserts * ins_c_64;
if (min_theo_d > max_d)
return max_d + 1;
if (ins_c + del_c < sub_c)
sub_c = ins_c + del_c;
max_theo_d = min_theo_d + sub_c * Min(m, n);
return levenshtein_result((int64) max_d + 1);
if (ins_c_64 + del_c_64 < sub_c_64)
sub_c_64 = ins_c_64 + del_c_64;
max_theo_d = min_theo_d + sub_c_64 * Min(m, n);
if (max_d >= max_theo_d)
max_d = -1;
else if (ins_c + del_c > 0)
else if (ins_c_64 + del_c_64 > 0)
{
/*
* Figure out how much of the first row of the notional matrix we
@@ -174,12 +177,12 @@ varstr_levenshtein(const char *source, int slen,
* column n - m. If we do start further right, the best-case
* total cost increases by ins_c + del_c for each move right.
*/
int slack_d = max_d - min_theo_d;
int64 slack_d = max_d - min_theo_d;
int best_column = net_inserts < 0 ? -net_inserts : 0;
int64 tmp;
stop_column = best_column + (slack_d / (ins_c + del_c)) + 1;
if (stop_column > m)
stop_column = m + 1;
tmp = best_column + (slack_d / (ins_c_64 + del_c_64)) + 1;
stop_column = Min(tmp, m + 1);
}
}
#endif
@@ -211,7 +214,7 @@ varstr_levenshtein(const char *source, int slen,
++n;
/* Previous and current rows of notional array. */
prev = (int *) palloc(2 * m * sizeof(int));
prev = (int64 *) palloc(2 * m * sizeof(int64));
curr = prev + m;
/*
@@ -219,12 +222,12 @@ varstr_levenshtein(const char *source, int slen,
* t, we must perform i deletions.
*/
for (int i = START_COLUMN; i < STOP_COLUMN; i++)
prev[i] = i * del_c;
prev[i] = i * del_c_64;
/* Loop through rows of the notional array */
for (y = target, j = 1; j < n; j++)
{
int *temp;
int64 *temp;
const char *x = source;
int y_char_len = n != tlen + 1 ? pg_mblen_range(y, tend) : 1;
int i;
@@ -239,7 +242,7 @@ varstr_levenshtein(const char *source, int slen,
*/
if (stop_column < m)
{
prev[stop_column] = max_d + 1;
prev[stop_column] = (int64) max_d + 1;
++stop_column;
}
@@ -251,13 +254,13 @@ varstr_levenshtein(const char *source, int slen,
*/
if (start_column == 0)
{
curr[0] = j * ins_c;
curr[0] = j * ins_c_64;
i = 1;
}
else
i = start_column;
#else
curr[0] = j * ins_c;
curr[0] = j * ins_c_64;
i = 1;
#endif
@@ -272,9 +275,9 @@ varstr_levenshtein(const char *source, int slen,
{
for (; i < STOP_COLUMN; i++)
{
int ins;
int del;
int sub;
int64 ins;
int64 del;
int64 sub;
int x_char_len = s_char_len[i - 1];
/*
@@ -286,14 +289,14 @@ varstr_levenshtein(const char *source, int slen,
* get past that test, then we compare the lengths and the
* remaining bytes.
*/
ins = prev[i] + ins_c;
del = curr[i - 1] + del_c;
ins = prev[i] + ins_c_64;
del = curr[i - 1] + del_c_64;
if (x[x_char_len - 1] == y[y_char_len - 1]
&& x_char_len == y_char_len &&
(x_char_len == 1 || rest_of_char_same(x, y, x_char_len)))
sub = prev[i - 1];
else
sub = prev[i - 1] + sub_c;
sub = prev[i - 1] + sub_c_64;
/* Take the one with minimum cost. */
curr[i] = Min(ins, del);
@@ -307,14 +310,14 @@ varstr_levenshtein(const char *source, int slen,
{
for (; i < STOP_COLUMN; i++)
{
int ins;
int del;
int sub;
int64 ins;
int64 del;
int64 sub;
/* Calculate costs for insertion, deletion, and substitution. */
ins = prev[i] + ins_c;
del = curr[i - 1] + del_c;
sub = prev[i - 1] + ((*x == *y) ? 0 : sub_c);
ins = prev[i] + ins_c_64;
del = curr[i - 1] + del_c_64;
sub = prev[i - 1] + ((*x == *y) ? 0 : sub_c_64);
/* Take the one with minimum cost. */
curr[i] = Min(ins, del);
@@ -360,8 +363,8 @@ varstr_levenshtein(const char *source, int slen,
int ii = stop_column - 1;
int net_inserts = ii - zp;
if (prev[ii] + (net_inserts > 0 ? net_inserts * ins_c :
-net_inserts * del_c) <= max_d)
if (prev[ii] + (net_inserts > 0 ? net_inserts * ins_c_64 :
-net_inserts * del_c_64) <= max_d)
break;
stop_column--;
}
@@ -372,8 +375,8 @@ varstr_levenshtein(const char *source, int slen,
int net_inserts = start_column - zp;
if (prev[start_column] +
(net_inserts > 0 ? net_inserts * ins_c :
-net_inserts * del_c) <= max_d)
(net_inserts > 0 ? net_inserts * ins_c_64 :
-net_inserts * del_c_64) <= max_d)
break;
/*
@@ -381,8 +384,8 @@ varstr_levenshtein(const char *source, int slen,
* there's nothing here that could confuse any future
* iteration of the outer loop.
*/
prev[start_column] = max_d + 1;
curr[start_column] = max_d + 1;
prev[start_column] = (int64) max_d + 1;
curr[start_column] = (int64) max_d + 1;
if (start_column != 0)
source += (s_char_len != NULL) ? s_char_len[start_column - 1] : 1;
start_column++;
@@ -390,7 +393,7 @@ varstr_levenshtein(const char *source, int slen,
/* If they cross, we're going to exceed the bound. */
if (start_column >= stop_column)
return max_d + 1;
return levenshtein_result((int64) max_d + 1);
}
#endif
}
@@ -399,5 +402,5 @@ varstr_levenshtein(const char *source, int slen,
* Because the final value was swapped from the previous row to the
* current row, that's where we'll find it.
*/
return prev[m - 1];
return levenshtein_result(prev[m - 1]);
}

View File

@@ -6132,6 +6132,20 @@ rest_of_char_same(const char *s1, const char *s2, int len)
return true;
}
/*
* Helper function for checking return value of Levenshtein distance functions.
* We calculate it as an int64, but the distance functions return an int32.
*/
static inline int
levenshtein_result(int64 res)
{
if (unlikely(res < PG_INT32_MIN || res > PG_INT32_MAX))
ereport(ERROR,
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
errmsg("levenshtein distance out of range")));
return res;
}
/* Expand each Levenshtein distance variant */
#include "levenshtein.c"
#define LEVENSHTEIN_LESS_EQUAL