Hi,
On 2021-03-10 20:26:56 -0800, Andres Freund wrote:
> > + shhashent = dshash_find_extended(pgStatSharedHash, &key,
> > +
> > create, nowait, create, &shfound);
> > + if (shhashent)
> > + {
> > + if (create && !shfound)
> > + {
> > + /* Create new stats entry. */
> > + dsa_pointer chunk = dsa_allocate0(area,
> > +
> > pgstat_sharedentsize[type]);
> > +
> > + shheader = dsa_get_address(area, chunk);
> > + LWLockInitialize(&shheader->lock, LWTRANCHE_STATS);
> > + pg_atomic_init_u32(&shheader->refcount, 0);
> > +
> > + /* Link the new entry from the hash entry. */
> > + shhashent->body = chunk;
> > + }
> > + else
> > + shheader = dsa_get_address(area, shhashent->body);
> > +
> > + /*
> > + * We expose this shared entry now. You might think that the
> > entry
> > + * can be removed by a concurrent backend, but since we are
> > creating
> > + * an stats entry, the object actually exists and used in the
> > upper
> > + * layer. Such an object cannot be dropped until the first
> > vacuum
> > + * after the current transaction ends.
> > + */
> > + dshash_release_lock(pgStatSharedHash, shhashent);
>
> I don't think you can safely release the lock before you incremented the
> refcount? What if, once the lock is released, somebody looks up that
> entry, increments the refcount, and decrements it again? It'll see a
> refcount of 0 at the end and decide to free the memory. Then the code
> below will access already freed / reused memory, no?
Yep, it's not even particularly hard to hit:
S0: CREATE TABLE a_table();
S0: INSERT INTO a_table();
S0: disconnect
S1: set a breakpoint to just after the dshash_release_lock(), with an
if objid == a_table_oid
S1: SELECT pg_stat_get_live_tuples('a_table'::regclass);
(will break at above breakpoint, without having incremented the
refcount yet)
S2: DROP TABLE a_table;
S2: VACUUM pg_class;
At that point S2's call to pgstat_vacuum_stat() will find the shared
stats entry for a_table, delete the entry from the shared hash table,
see that the stats data has a zero refcount, and free it. Once S1 wakes
up it'll use already freed (and potentially since reused) memory.
Greetings,
Andres Freund