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


Reply via email to