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