merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

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:
Msrc/backenddb/pg_update_inventory_locked.sql | 38+++++++++++++++++++++++++++++++-------
Msrc/backenddb/test_merchantdb.c | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
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; }