merchant

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

commit 03ae6e414c8ea3a8b0572c0144a687dca1691348
parent 9dee32f283879f33ad216d676b60e8ee909e36de
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sun,  9 Aug 2026 13:11:18 +0200

db: handle INT64_MIN negation and cap the rows returned per query

Negating the signed limit is undefined for INT64_MIN and the wrapped
value reached Postgres as a negative LIMIT; the /private/products,
/transfers, /incoming and /tokens handlers pass the client's limit
through unclamped, so this was remotely reachable.

Diffstat:
Msrc/backenddb/helper.h | 24++++++++++++++++++++++++
Msrc/backenddb/iterate_expected_transfers.c | 11++++++++++-
Msrc/backenddb/iterate_login_tokens.c | 11++++++++++-
Msrc/backenddb/iterate_money_pots.c | 10+++++++++-
Msrc/backenddb/iterate_product_groups.c | 10+++++++++-
Msrc/backenddb/iterate_products.c | 11++++++++++-
Msrc/backenddb/iterate_reports.c | 10+++++++++-
Msrc/backenddb/iterate_transfers.c | 10+++++++++-
8 files changed, 90 insertions(+), 7 deletions(-)

diff --git a/src/backenddb/helper.h b/src/backenddb/helper.h @@ -81,6 +81,30 @@ extern uint64_t TMH_PG_prep_gen_; /** + * Convert the signed @a limit of a paginated listing into the row + * limit to pass to SQL. The sign selects the iteration direction and + * the magnitude the number of rows, but computing `-limit` directly is + * undefined behaviour for `INT64_MIN`, and the resulting value would + * reach Postgres as `LIMIT -9223372036854775808`, which errors out. + * `INT64_MIN` is therefore saturated to `INT64_MAX`. + * + * @param limit signed limit as given by the caller + * @return absolute value of @a limit, without signed overflow + */ +static inline uint64_t +TALER_MERCHANTDB_abs_limit (int64_t limit) +{ + /* Postgres binds INT8 as a *signed* 64-bit integer, so a magnitude + above INT64_MAX would reach LIMIT as a negative number. */ + if (INT64_MIN == limit) + return (uint64_t) INT64_MAX; + if (limit < 0) + return (uint64_t) -limit; + return (uint64_t) limit; +} + + +/** * Prepares SQL statement @a sql under @a name for * connection @a pg once. * Returns with #GNUNET_DB_STATUS_HARD_ERROR on failure. diff --git a/src/backenddb/iterate_expected_transfers.c b/src/backenddb/iterate_expected_transfers.c @@ -24,6 +24,14 @@ #include "helper.h" #include <microhttpd.h> /* for HTTP status codes */ + +/** + * Hard upper bound on the number of records returned by a single + * call, regardless of the limit requested by the client. + */ +#define MAX_RECORDS 50000 + + /** * Closure for #lookup_expected_transfers_cb(). */ @@ -161,7 +169,8 @@ TALER_MERCHANTDB_iterate_expected_transfers ( TALER_MERCHANTDB_IncomingCallback cb, void *cb_cls) { - uint64_t plimit = (uint64_t) ((limit < 0) ? -limit : limit); + uint64_t plimit = GNUNET_MIN ((uint64_t) MAX_RECORDS, + TALER_MERCHANTDB_abs_limit (limit)); bool by_time = ( (! GNUNET_TIME_absolute_is_never (before.abs_time)) || (! GNUNET_TIME_absolute_is_zero (after.abs_time)) ); struct LookupExpectedTransfersContext ltc = { diff --git a/src/backenddb/iterate_login_tokens.c b/src/backenddb/iterate_login_tokens.c @@ -23,6 +23,14 @@ #include "merchant-database/iterate_login_tokens.h" #include "helper.h" + +/** + * Hard upper bound on the number of records returned by a single + * call, regardless of the limit requested by the client. + */ +#define MAX_RECORDS 50000 + + /** * Context used for TALER_MERCHANTDB_iterate_products(). */ @@ -114,7 +122,8 @@ TALER_MERCHANTDB_iterate_login_tokens ( void *cb_cls) { struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); - uint64_t plimit = (uint64_t) ((limit < 0) ? -limit : limit); + uint64_t plimit = GNUNET_MIN ((uint64_t) MAX_RECORDS, + TALER_MERCHANTDB_abs_limit (limit)); struct LookupLoginTokensContext plc = { .cb = cb, .cb_cls = cb_cls, diff --git a/src/backenddb/iterate_money_pots.c b/src/backenddb/iterate_money_pots.c @@ -25,6 +25,13 @@ /** + * Hard upper bound on the number of records returned by a single + * call, regardless of the limit requested by the client. + */ +#define MAX_RECORDS 50000 + + +/** * Context used for TALER_MERCHANTDB_iterate_money_pots(). */ struct LookupMoneyPotsContext @@ -113,7 +120,8 @@ TALER_MERCHANTDB_iterate_money_pots ( TALER_MERCHANTDB_MoneyPotsCallback cb, void *cb_cls) { - uint64_t plimit = (uint64_t) ((limit < 0) ? -limit : limit); + uint64_t plimit = GNUNET_MIN ((uint64_t) MAX_RECORDS, + TALER_MERCHANTDB_abs_limit (limit)); struct LookupMoneyPotsContext plc = { .pg = pg, .cb = cb, diff --git a/src/backenddb/iterate_product_groups.c b/src/backenddb/iterate_product_groups.c @@ -25,6 +25,13 @@ /** + * Hard upper bound on the number of records returned by a single + * call, regardless of the limit requested by the client. + */ +#define MAX_RECORDS 50000 + + +/** * Context used for TALER_MERCHANTDB_iterate_product_groups(). */ struct LookupProductGroupsContext @@ -103,7 +110,8 @@ TALER_MERCHANTDB_iterate_product_groups ( TALER_MERCHANTDB_ProductGroupsCallback cb, void *cb_cls) { - uint64_t plimit = (uint64_t) ((limit < 0) ? -limit : limit); + uint64_t plimit = GNUNET_MIN ((uint64_t) MAX_RECORDS, + TALER_MERCHANTDB_abs_limit (limit)); struct LookupProductGroupsContext plc = { .cb = cb, .cb_cls = cb_cls, diff --git a/src/backenddb/iterate_products.c b/src/backenddb/iterate_products.c @@ -23,6 +23,14 @@ #include "merchant-database/iterate_products.h" #include "helper.h" + +/** + * Hard upper bound on the number of records returned by a single + * call, regardless of the limit requested by the client. + */ +#define MAX_RECORDS 50000 + + /** * Context used for TALER_MERCHANTDB_iterate_products(). */ @@ -102,7 +110,8 @@ TALER_MERCHANTDB_iterate_products ( TALER_MERCHANTDB_ProductsCallback cb, void *cb_cls) { - uint64_t plimit = (uint64_t) ((limit < 0) ? -limit : limit); + uint64_t plimit = GNUNET_MIN ((uint64_t) MAX_RECORDS, + TALER_MERCHANTDB_abs_limit (limit)); struct LookupProductsContext plc = { .cb = cb, .cb_cls = cb_cls, diff --git a/src/backenddb/iterate_reports.c b/src/backenddb/iterate_reports.c @@ -25,6 +25,13 @@ /** + * Hard upper bound on the number of records returned by a single + * call, regardless of the limit requested by the client. + */ +#define MAX_RECORDS 50000 + + +/** * Context used for TALER_MERCHANTDB_iterate_reports(). */ struct SelectReportsContext @@ -103,7 +110,8 @@ TALER_MERCHANTDB_iterate_reports ( TALER_MERCHANTDB_ReportsCallback cb, void *cb_cls) { - uint64_t plimit = (uint64_t) ((limit < 0) ? -limit : limit); + uint64_t plimit = GNUNET_MIN ((uint64_t) MAX_RECORDS, + TALER_MERCHANTDB_abs_limit (limit)); struct SelectReportsContext plc = { .cb = cb, .cb_cls = cb_cls, diff --git a/src/backenddb/iterate_transfers.c b/src/backenddb/iterate_transfers.c @@ -25,6 +25,13 @@ /** + * Hard upper bound on the number of records returned by a single + * call, regardless of the limit requested by the client. + */ +#define MAX_RECORDS 50000 + + +/** * Closure for #lookup_transfers_cb(). */ struct LookupTransfersContext @@ -136,7 +143,8 @@ TALER_MERCHANTDB_iterate_transfers ( TALER_MERCHANTDB_TransferCallback cb, void *cb_cls) { - uint64_t plimit = (uint64_t) ((limit < 0) ? -limit : limit); + uint64_t plimit = GNUNET_MIN ((uint64_t) MAX_RECORDS, + TALER_MERCHANTDB_abs_limit (limit)); bool by_time = ( (! GNUNET_TIME_absolute_is_never (before.abs_time)) || (! GNUNET_TIME_absolute_is_zero (after.abs_time)) ); struct LookupTransfersContext ltc = {