commit 0e4fda08fd481b02d49ac814e96c46a529219ebd
parent b04ee597bb545670e2429bd3fce2f9c644fa1ce8
Author: Christian Grothoff <christian@grothoff.org>
Date: Wed, 5 Aug 2026 23:28:41 +0200
handle lock amount integer overflow
Diffstat:
2 files changed, 88 insertions(+), 27 deletions(-)
diff --git a/src/backenddb/pg_update_inventory_locked.sql b/src/backenddb/pg_update_inventory_locked.sql
@@ -25,6 +25,10 @@ AS $$
DECLARE
my_product_serial INT8;
my_delta NUMERIC := 0;
+ my_total NUMERIC;
+ -- Largest amount representable by (total_locked, total_locked_frac).
+ my_max CONSTANT NUMERIC := 9223372036854775807::NUMERIC * 1000000
+ + 999999;
BEGIN
CASE TG_OP
WHEN 'INSERT' THEN
@@ -46,16 +50,36 @@ BEGIN
END CASE;
IF (my_delta <> 0)
THEN
+ SELECT total_locked::NUMERIC * 1000000
+ + total_locked_frac::NUMERIC
+ INTO my_total
+ FROM merchant_inventory
+ WHERE product_serial = my_product_serial
+ FOR UPDATE;
+ IF NOT FOUND
+ THEN
+ -- Product is already gone (cascading delete), nothing to adjust.
+ RETURN NULL;
+ END IF;
+ my_total := my_total + my_delta;
+ -- Saturate rather than raise 'bigint out of range' (22003):
+ -- products carrying the INT64_MAX "unlimited stock" sentinel skip
+ -- the availability check, so their locks can sum up to more than
+ -- INT8 can hold. The counter is advisory, a 500 would not be.
+ IF (my_total < 0)
+ THEN
+ my_total := 0;
+ END IF;
+ IF (my_total > my_max)
+ THEN
+ my_total := my_max;
+ END IF;
-- Use div()/% (truncating, never rounding) to split the combined
-- micro-unit amount back into whole and fractional parts.
UPDATE merchant_inventory
- SET total_locked =
- div (total_locked::NUMERIC * 1000000
- + total_locked_frac::NUMERIC + my_delta,
- 1000000)::INT8,
- total_locked_frac =
- ((total_locked::NUMERIC * 1000000
- + total_locked_frac::NUMERIC + my_delta) % 1000000)::INT4
+ SET total_locked = div (my_total,
+ 1000000)::INT8,
+ total_locked_frac = (my_total % 1000000)::INT4
WHERE product_serial = my_product_serial;
END IF;
RETURN NULL;
diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c
@@ -2178,9 +2178,10 @@ struct TestLocks_Closure
struct InstanceData instance;
/**
- * The product we lock.
+ * The products we lock; [0] has a limited stock, [1] carries the
+ * INT64_MAX "unlimited stock" sentinel.
*/
- struct ProductData product;
+ struct ProductData products[2];
};
@@ -2284,8 +2285,11 @@ pre_test_locks (struct TestLocks_Closure *cls)
make_instance ("test_inst_locks",
&cls->instance);
make_product ("test_locks_pd_0",
- &cls->product);
- cls->product.product.total_stock = 100;
+ &cls->products[0]);
+ cls->products[0].product.total_stock = 100;
+ make_product ("test_locks_pd_1",
+ &cls->products[1]);
+ cls->products[1].product.total_stock = INT64_MAX;
}
@@ -2298,7 +2302,8 @@ static void
post_test_locks (struct TestLocks_Closure *cls)
{
free_instance_data (&cls->instance);
- free_product_data (&cls->product);
+ free_product_data (&cls->products[0]);
+ free_product_data (&cls->products[1]);
}
@@ -2325,7 +2330,7 @@ run_test_locks (struct TestLocks_Closure *cls)
TEST_RET_ON_FAIL (test_insert_instance (&cls->instance,
GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
TEST_RET_ON_FAIL (test_insert_product (&cls->instance,
- &cls->product,
+ &cls->products[0],
0,
NULL,
GNUNET_DB_STATUS_SUCCESS_ONE_RESULT,
@@ -2335,67 +2340,67 @@ run_test_locks (struct TestLocks_Closure *cls)
/* Lock 40 of the 100 units in stock */
TEST_RET_ON_FAIL (test_insert_inventory_lock (
&cls->instance,
- &cls->product,
+ &cls->products[0],
&uuid1,
40,
GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
TEST_RET_ON_FAIL (test_product_locked (&cls->instance,
- &cls->product,
+ &cls->products[0],
40,
0));
/* Re-posting a lock for the same UUID *updates* the existing lock,
the locks must not stack up. */
TEST_RET_ON_FAIL (test_insert_inventory_lock (
&cls->instance,
- &cls->product,
+ &cls->products[0],
&uuid1,
10,
GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
TEST_RET_ON_FAIL (test_product_locked (&cls->instance,
- &cls->product,
+ &cls->products[0],
10,
0));
/* A second lock competes with the first one for the remaining stock */
TEST_RET_ON_FAIL (test_insert_inventory_lock (
&cls->instance,
- &cls->product,
+ &cls->products[0],
&uuid2,
91,
GNUNET_DB_STATUS_SUCCESS_NO_RESULTS));
TEST_RET_ON_FAIL (test_product_locked (&cls->instance,
- &cls->product,
+ &cls->products[0],
10,
0));
TEST_RET_ON_FAIL (test_insert_inventory_lock (
&cls->instance,
- &cls->product,
+ &cls->products[0],
&uuid2,
90,
GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
TEST_RET_ON_FAIL (test_product_locked (&cls->instance,
- &cls->product,
+ &cls->products[0],
100,
0));
/* Locking a quantity of zero releases the lock */
TEST_RET_ON_FAIL (test_insert_inventory_lock (
&cls->instance,
- &cls->product,
+ &cls->products[0],
&uuid1,
0,
GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
TEST_RET_ON_FAIL (test_product_locked (&cls->instance,
- &cls->product,
+ &cls->products[0],
90,
0));
/* Unlocking an UUID that holds no lock is a no-op, not an error */
TEST_RET_ON_FAIL (test_insert_inventory_lock (
&cls->instance,
- &cls->product,
+ &cls->products[0],
&uuid1,
0,
GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
TEST_RET_ON_FAIL (test_product_locked (&cls->instance,
- &cls->product,
+ &cls->products[0],
90,
0));
/* Explicitly dropping the remaining lock frees the stock again */
@@ -2406,12 +2411,12 @@ run_test_locks (struct TestLocks_Closure *cls)
&uuid2),
"Unlock inventory failed\n");
TEST_RET_ON_FAIL (test_product_locked (&cls->instance,
- &cls->product,
+ &cls->products[0],
0,
0));
/* Locking an unknown product is reported as 'no results' */
{
- struct ProductData unknown = cls->product;
+ struct ProductData unknown = cls->products[0];
unknown.id = "test_locks_pd_unknown";
TEST_RET_ON_FAIL (test_insert_inventory_lock (
@@ -2421,6 +2426,38 @@ run_test_locks (struct TestLocks_Closure *cls)
1,
GNUNET_DB_STATUS_SUCCESS_NO_RESULTS));
}
+ /* Products with the INT64_MAX "unlimited stock" sentinel skip the
+ availability check, so two large locks can exceed what the
+ total_locked counter can represent. That must saturate, not blow
+ up with 'bigint out of range' (which the caller turns into a 500). */
+ TEST_RET_ON_FAIL (test_insert_product (&cls->instance,
+ &cls->products[1],
+ 0,
+ NULL,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT,
+ false,
+ false,
+ -1));
+ TEST_RET_ON_FAIL (test_insert_inventory_lock (
+ &cls->instance,
+ &cls->products[1],
+ &uuid1,
+ INT64_MAX,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
+ TEST_RET_ON_FAIL (test_product_locked (&cls->instance,
+ &cls->products[1],
+ INT64_MAX,
+ 0));
+ TEST_RET_ON_FAIL (test_insert_inventory_lock (
+ &cls->instance,
+ &cls->products[1],
+ &uuid2,
+ INT64_MAX,
+ GNUNET_DB_STATUS_SUCCESS_ONE_RESULT));
+ TEST_RET_ON_FAIL (test_product_locked (&cls->instance,
+ &cls->products[1],
+ INT64_MAX,
+ 999999));
return 0;
}