On Wed Aug 5, 2026 at 4:05 PM UTC, Peter Eisentraut wrote:
> On 30.07.26 00:07, Tristan Partin wrote:
>> The counted_by[0] compiler attribute is fairly new. It was added in GCC
>> 15 and Clang 18. It has been used fairly extensively in the Linux
>> kernel[0].
>> 
>> To summarize the benefits of the attribute:
>> 
>> - Runtime bounds checking with -DFORTIFY_SOURCE=3 and -fsanitize-bounds
>> - Accurate reporting of __builtin_dynamic_object_size()
>> 
>> While we don't use __builtin_dynamic_object_size(), I think the runtime
>> bounds checking improvements are easily worth the little bit of effort
>> to add the attribute in various locations and review the code. I think
>> it will improve things for buildfarm animals using ASan due to expanded
>> coverage.
>
> I think this is a good idea.
>
> Maybe you could produce an intentionally broken piece of code that would 
> illustrate what kinds of reports one might hope to get from this.

Sure. I'll share three examples.

Back in 2022, Tom committed a fix for an off by one error: 
a36196972b[0]. Assuming we had an animal configured for 
-fsanitize=address, and the accompanying diff (logical-replication.diff) 
applied, we could have probably done a better job at avoiding this 
mistake in the first place. The animal would have rightfully complained 
about this mistake.

Another example: TupleDescCompactAttr() is called into over 100 times 
and takes an index into the TupleDesc::compact_attrs array. That 
function does not contain an Assert() for checking if the index is 
actually valid. An errant caller could easily cause a SIGSEGV or we 
could reach into uninitialized memory. In the supplied diff 
(tuple.diff), I added the counted_by attribute, which would allow us to 
fail loudly if an animal was configured with -fsanitize=address. I also 
went ahead and also added an Assert() just to be safe.

To give a more concrete example, I have attached a C file that has 
a similar off by one mistake to the one that Tom fixed. You can compile 
it with the following command line:

        gcc -fsanitize=address test.c

And if you run it (./a.out), you will see something like the following:

        $ ./a.out
        0
        0
        0
        =================================================================
        ==1651747==ERROR: AddressSanitizer: heap-buffer-overflow on address 
0x7bf02dde001c at pc 0x55d9fecc52a2 bp 0x7ffcaf3ff4a0 sp 0x7ffcaf3ff498
        READ of size 4 at 0x7bf02dde001c thread T0
            #0 0x55d9fecc52a1 in main 
(/local/home/dbltap/Projects/postgres/counted_by/a.out+0x12a1)
            #1 0x7fd02ee2b284 in __libc_start_call_main 
(/nix/store/qqiqd3ah10x8hzsif4j1y4xc1miw23nx-glibc-2.42-67/lib/libc.so.6+0x2b284)
 (BuildId: c38f504d883365a4836730fb07e9269ade763988)
            #2 0x7fd02ee2b337 in __libc_start_main@GLIBC_2.2.5 
(/nix/store/qqiqd3ah10x8hzsif4j1y4xc1miw23nx-glibc-2.42-67/lib/libc.so.6+0x2b337)
 (BuildId: c38f504d883365a4836730fb07e9269ade763988)
            #3 0x55d9fecc50e4 in _start 
(/local/home/dbltap/Projects/postgres/counted_by/a.out+0x10e4)
        
        0x7bf02dde001c is located 0 bytes after 12-byte region 
[0x7bf02dde0010,0x7bf02dde001c)
        allocated by thread T0 here:
            #0 0x7fd02f32be4f in calloc 
(/nix/store/lv6lq05xay6zr2lbchz47zs8yrza3y86-gcc-16.2.0-lib/lib/libasan.so.8+0x12be4f)
            #1 0x55d9fecc524a in main 
(/local/home/dbltap/Projects/postgres/counted_by/a.out+0x124a)
            #2 0x7fd02ee2b284 in __libc_start_call_main 
(/nix/store/qqiqd3ah10x8hzsif4j1y4xc1miw23nx-glibc-2.42-67/lib/libc.so.6+0x2b284)
 (BuildId: c38f504d883365a4836730fb07e9269ade763988)
            #3 0x7fd02ee2b337 in __libc_start_main@GLIBC_2.2.5 
(/nix/store/qqiqd3ah10x8hzsif4j1y4xc1miw23nx-glibc-2.42-67/lib/libc.so.6+0x2b337)
 (BuildId: c38f504d883365a4836730fb07e9269ade763988)
            #4 0x55d9fecc50e4 in _start 
(/local/home/dbltap/Projects/postgres/counted_by/a.out+0x10e4)

[0]: https://github.com/postgres/postgres/commit/a36196972b

-- 
Tristan Partin
PostgreSQL Contributors Team
AWS (https://aws.amazon.com)
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

const size_t total = 3;

struct Array {
	size_t ndata;
	int *data;
};

int
main(void)
{
	struct Array a = { .ndata = 3 };
	a.data = calloc(a.ndata, sizeof(a.data[0]));

	for (size_t i = 0; i <= total; i++) {
		printf("%d\n", a.data[i]);
	}

	return 0;
}
diff --git i/src/backend/replication/logical/launcher.c w/src/backend/replication/logical/launcher.c
index 313e31ff2e3..e680c5a6966 100644
--- i/src/backend/replication/logical/launcher.c
+++ w/src/backend/replication/logical/launcher.c
@@ -67,7 +67,8 @@ typedef struct LogicalRepCtxStruct
 	dshash_table_handle last_start_dsh;
 
 	/* Background workers. */
-	LogicalRepWorker workers[FLEXIBLE_ARRAY_MEMBER];
+	int		nworkers;
+	LogicalRepWorker workers[FLEXIBLE_ARRAY_MEMBER] pg_attribute_counted_by(nworkers);
 } LogicalRepCtxStruct;
 
 static LogicalRepCtxStruct *LogicalRepCtx;
@@ -276,7 +277,7 @@ logicalrep_worker_find(LogicalRepWorkerType wtype, Oid subid, Oid relid,
 	Assert(LWLockHeldByMe(LogicalRepWorkerLock));
 
 	/* Search for an attached worker that matches the specified criteria. */
-	for (i = 0; i < max_logical_replication_workers; i++)
+	for (i = 0; i < LogicalRepCtx->nworkers; i++)
 	{
 		LogicalRepWorker *w = &LogicalRepCtx->workers[i];
 
@@ -311,7 +312,7 @@ logicalrep_workers_find(Oid subid, bool only_running, bool acquire_lock)
 	Assert(LWLockHeldByMe(LogicalRepWorkerLock));
 
 	/* Search for attached worker for a given subscription id. */
-	for (i = 0; i < max_logical_replication_workers; i++)
+	for (i = 0; i < LogicalRepCtx->nworkers; i++)
 	{
 		LogicalRepWorker *w = &LogicalRepCtx->workers[i];
 
@@ -380,7 +381,7 @@ logicalrep_worker_launch(LogicalRepWorkerType wtype,
 
 retry:
 	/* Find unused worker slot. */
-	for (i = 0; i < max_logical_replication_workers; i++)
+	for (i = 0; i < LogicalRepCtx->nworkers; i++)
 	{
 		LogicalRepWorker *w = &LogicalRepCtx->workers[i];
 
@@ -405,7 +406,7 @@ logicalrep_worker_launch(LogicalRepWorkerType wtype,
 	{
 		bool		did_cleanup = false;
 
-		for (i = 0; i < max_logical_replication_workers; i++)
+		for (i = 0; i < LogicalRepCtx->nworkers; i++)
 		{
 			LogicalRepWorker *w = &LogicalRepCtx->workers[i];
 
@@ -697,7 +698,7 @@ logicalrep_pa_worker_stop(ParallelApplyWorkerInfo *winfo)
 	slot_no = winfo->shared->logicalrep_worker_slot_no;
 	SpinLockRelease(&winfo->shared->mutex);
 
-	Assert(slot_no >= 0 && slot_no < max_logical_replication_workers);
+	Assert(slot_no >= 0 && slot_no < LogicalRepCtx->nworkers);
 
 	/*
 	 * Detach from the error_mq_handle for the parallel apply worker before
@@ -769,7 +770,7 @@ logicalrep_worker_attach(int slot)
 	/* Block concurrent access. */
 	LWLockAcquire(LogicalRepWorkerLock, LW_EXCLUSIVE);
 
-	Assert(slot >= 0 && slot < max_logical_replication_workers);
+	Assert(slot >= 0 && slot < LogicalRepCtx->nworkers);
 	MyLogicalRepWorker = &LogicalRepCtx->workers[slot];
 
 	if (!MyLogicalRepWorker->in_use)
@@ -942,7 +943,7 @@ logicalrep_sync_worker_count(Oid subid)
 	Assert(LWLockHeldByMe(LogicalRepWorkerLock));
 
 	/* Search for attached worker for a given subscription id. */
-	for (i = 0; i < max_logical_replication_workers; i++)
+	for (i = 0; i < LogicalRepCtx->nworkers; i++)
 	{
 		LogicalRepWorker *w = &LogicalRepCtx->workers[i];
 
@@ -969,7 +970,7 @@ logicalrep_pa_worker_count(Oid subid)
 	 * Scan all attached parallel apply workers, only counting those which
 	 * have the given subscription id.
 	 */
-	for (i = 0; i < max_logical_replication_workers; i++)
+	for (i = 0; i < LogicalRepCtx->nworkers; i++)
 	{
 		LogicalRepWorker *w = &LogicalRepCtx->workers[i];
 
@@ -1049,9 +1050,10 @@ ApplyLauncherShmemInit(void *arg)
 
 	LogicalRepCtx->last_start_dsa = DSA_HANDLE_INVALID;
 	LogicalRepCtx->last_start_dsh = DSHASH_HANDLE_INVALID;
+	LogicalRepCtx->nworkers = max_logical_replication_workers;
 
 	/* Initialize memory and spin locks for each worker slot. */
-	for (slot = 0; slot < max_logical_replication_workers; slot++)
+	for (slot = 0; slot < LogicalRepCtx->nworkers; slot++)
 	{
 		LogicalRepWorker *worker = &LogicalRepCtx->workers[slot];
 
@@ -1602,7 +1604,7 @@ GetLeaderApplyWorkerPid(pid_t pid)
 
 	LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-	for (i = 0; i < max_logical_replication_workers; i++)
+	for (i = 0; i < LogicalRepCtx->nworkers; i++)
 	{
 		LogicalRepWorker *w = &LogicalRepCtx->workers[i];
 
@@ -1634,7 +1636,7 @@ pg_stat_get_subscription(PG_FUNCTION_ARGS)
 	/* Make sure we get consistent view of the workers. */
 	LWLockAcquire(LogicalRepWorkerLock, LW_SHARED);
 
-	for (i = 0; i < max_logical_replication_workers; i++)
+	for (i = 0; i < LogicalRepCtx->nworkers; i++)
 	{
 		/* for each row */
 		Datum		values[PG_STAT_GET_SUBSCRIPTION_COLS] = {0};
diff --git i/src/include/access/tupdesc.h w/src/include/access/tupdesc.h
index d26287271e9..887088f3ae1 100644
--- i/src/include/access/tupdesc.h
+++ w/src/include/access/tupdesc.h
@@ -158,7 +158,7 @@ typedef struct TupleDescData
 										 * compact_attrs element. */
 	TupleConstr *constr;		/* constraints, or NULL if none */
 	/* compact_attrs[N] is the compact metadata of Attribute Number N+1 */
-	CompactAttribute compact_attrs[FLEXIBLE_ARRAY_MEMBER];
+	CompactAttribute compact_attrs[FLEXIBLE_ARRAY_MEMBER] pg_attribute_counted_by(natts);
 }			TupleDescData;
 typedef struct TupleDescData *TupleDesc;
 
@@ -194,7 +194,11 @@ extern void verify_compact_attribute(TupleDesc, int attnum);
 static inline CompactAttribute *
 TupleDescCompactAttr(TupleDesc tupdesc, int i)
 {
-	CompactAttribute *cattr = &tupdesc->compact_attrs[i];
+	CompactAttribute *cattr;
+
+	Assert(i >= 0 && i < tupdesc->natts);
+
+	cattr = &tupdesc->compact_attrs[i];
 
 #ifdef USE_ASSERT_CHECKING
 

Reply via email to