Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/debug_point.c
Original file line number Diff line number Diff line change
Expand Up @@ -289,3 +289,38 @@ ts_debug_point_raise_error_if_enabled(const char *name)
(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
errmsg("error injected at debug point '%s'", point.name)));
}

/*
* One-shot variant: release the debug point before raising the error.
* This allows error recovery code paths to call the same function
* without hitting the injection again.
*/
void
ts_debug_point_raise_error_oneshot(const char *name)
{
DebugPoint point;
LockAcquireResult lock_acquire_result;

debug_point_init(&point, name);

lock_acquire_result = LockAcquire(&point.tag, ShareLock, true, true);
switch (lock_acquire_result)
{
case LOCKACQUIRE_OK:
LockRelease(&point.tag, ShareLock, true);
if (LockHeldByMeCompat(&point.tag, ExclusiveLock, false))
break;
return;
case LOCKACQUIRE_ALREADY_HELD:
case LOCKACQUIRE_ALREADY_CLEAR:
LockRelease(&point.tag, ShareLock, true);
return;
case LOCKACQUIRE_NOT_AVAIL:
break;
}

debug_point_release(&point);
ereport(ERROR,
(errcode(ERRCODE_TRIGGERED_ACTION_EXCEPTION),
errmsg("error injected at debug point '%s'", point.name)));
}
3 changes: 3 additions & 0 deletions src/debug_point.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,20 @@

extern TSDLLEXPORT void ts_debug_point_wait(const char *name, bool blocking);
extern TSDLLEXPORT void ts_debug_point_raise_error_if_enabled(const char *name);
extern TSDLLEXPORT void ts_debug_point_raise_error_oneshot(const char *name);

#ifdef TS_DEBUG

#define DEBUG_WAITPOINT(NAME) ts_debug_point_wait((NAME), true)
#define DEBUG_RETRY_WAITPOINT(NAME) ts_debug_point_wait((NAME), false)
#define DEBUG_ERROR_INJECTION(NAME) ts_debug_point_raise_error_if_enabled((NAME))
#define DEBUG_ERROR_INJECTION_ONESHOT(NAME) ts_debug_point_raise_error_oneshot((NAME))

#else

#define DEBUG_WAITPOINT(NAME)
#define DEBUG_RETRY_WAITPOINT(NAME)
#define DEBUG_ERROR_INJECTION(NAME)
#define DEBUG_ERROR_INJECTION_ONESHOT(NAME)

#endif
4 changes: 4 additions & 0 deletions src/hypertable_cache.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <utils/builtins.h>
#include <utils/catcache.h>
#include <utils/lsyscache.h>
#include <utils/memutils.h>

#include "cache.h"
#include "dimension.h"
Expand All @@ -21,6 +22,8 @@
static void *hypertable_cache_create_entry(Cache *cache, CacheQuery *query);
static void hypertable_cache_missing_error(const Cache *cache, const CacheQuery *query);

#include "debug_point.h"

typedef struct HypertableCacheQuery
{
CacheQuery q;
Expand Down Expand Up @@ -52,6 +55,7 @@ hypertable_cache_valid_result(const void *result)
static Cache *
hypertable_cache_create()
{
DEBUG_ERROR_INJECTION_ONESHOT("hypertable-cache-create");
MemoryContext ctx =
AllocSetContextCreate(CacheMemoryContext, "Hypertable cache", ALLOCSET_DEFAULT_SIZES);

Expand Down
42 changes: 42 additions & 0 deletions test/expected/cache_invalidate_uaf.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
-- This file and its contents are licensed under the Apache License 2.0.
-- Please see the included NOTICE for copyright information and
-- LICENSE-APACHE for a copy of the license.
-- Test for cache pointer use-after-free on invalidation with active pins. We
-- reproduce this by triggering the cache invalidation from a trigger.
\c :TEST_DBNAME :ROLE_SUPERUSER
CREATE TABLE cache_inval_test(time timestamptz NOT NULL, val int);
SELECT create_hypertable('cache_inval_test', 'time');
create_hypertable
-------------------------------
(1,public,cache_inval_test,t)

INSERT INTO cache_inval_test VALUES ('2024-01-01', 1);
CREATE FUNCTION trg_inval_cache() RETURNS trigger AS $$
BEGIN
PERFORM set_chunk_time_interval('cache_inval_test', INTERVAL '2 days');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_inval BEFORE INSERT ON cache_inval_test
FOR EACH ROW EXECUTE FUNCTION trg_inval_cache();
SELECT debug_waitpoint_enable('hypertable-cache-create');
debug_waitpoint_enable
------------------------


BEGIN;
SAVEPOINT sp;
\set ON_ERROR_STOP 0
INSERT INTO cache_inval_test VALUES ('2024-01-02', 2);
ERROR: error injected at debug point 'hypertable-cache-create'
\set ON_ERROR_STOP 1
ROLLBACK TO sp;
INSERT INTO cache_inval_test VALUES ('2024-01-03', 3);
COMMIT;
SELECT val FROM cache_inval_test ORDER BY time;
val
-----
1
3

DROP TABLE cache_inval_test;
1 change: 1 addition & 0 deletions test/sql/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ if(CMAKE_BUILD_TYPE MATCHES Debug)
alter_table_memory.sql
bgw_launcher.sql
c_unit_tests.sql
cache_invalidate_uaf.sql
copy_memory_usage.sql
metadata.sql
multi_transaction_index.sql
Expand Down
40 changes: 40 additions & 0 deletions test/sql/cache_invalidate_uaf.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
-- This file and its contents are licensed under the Apache License 2.0.
-- Please see the included NOTICE for copyright information and
-- LICENSE-APACHE for a copy of the license.

-- Test for cache pointer use-after-free on invalidation with active pins. We
-- reproduce this by triggering the cache invalidation from a trigger.

\c :TEST_DBNAME :ROLE_SUPERUSER

CREATE TABLE cache_inval_test(time timestamptz NOT NULL, val int);
SELECT create_hypertable('cache_inval_test', 'time');
INSERT INTO cache_inval_test VALUES ('2024-01-01', 1);

CREATE FUNCTION trg_inval_cache() RETURNS trigger AS $$
BEGIN
PERFORM set_chunk_time_interval('cache_inval_test', INTERVAL '2 days');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_inval BEFORE INSERT ON cache_inval_test
FOR EACH ROW EXECUTE FUNCTION trg_inval_cache();

SELECT debug_waitpoint_enable('hypertable-cache-create');

BEGIN;
SAVEPOINT sp;

\set ON_ERROR_STOP 0
INSERT INTO cache_inval_test VALUES ('2024-01-02', 2);
\set ON_ERROR_STOP 1

ROLLBACK TO sp;

INSERT INTO cache_inval_test VALUES ('2024-01-03', 3);
COMMIT;

SELECT val FROM cache_inval_test ORDER BY time;

DROP TABLE cache_inval_test;
Loading