[PATCH v2 4/4] libio: Fix race access to _IO_FLAGS2_NEED_LOCK
Adhemerval Zanella
adhemerval.zanella@linaro.org
Mon May 10 21:42:57 GMT 2021
On 10/05/2021 16:33, Florian Weimer wrote:
> * Adhemerval Zanella:
>
>> On 10/05/2021 16:18, Florian Weimer wrote:
>>> * Adhemerval Zanella:
>>>
>>>> I see that my first assumption of moving the _IO_FLAGS2_NEED_LOCK after
>>>> the lock should be suffice. The data race is not really related to
>>>> _IO_FLAGS2_NEED_LOCK set in fact, but rather the small window where
>>>> the _flags2 read and update with _IO_FLAGS2_NEED_LOCK might lost some
>>>> bits update.
>>>
>>> Right.
>>>
>>>> Maybe the comment:
>>>>
>>>> /* The _IO_FLAGS2_NEED_LOCK is required here to enable the non-atomic
>>>> fast path on function that call _IO_need_lock. However it requires
>>>> to be set with stream locked to avoid a potential data race. */
>>>
>>> s/fast path/path/ I suppose, and “it necessary to set it with the stream
>>> locked”.
>>
>>
>> /* The _IO_FLAGS2_NEED_LOCK is required here to enable the non-atomic
>> path on function that call _IO_need_lock. However it is necessary
>> to set it with the stream locked to avoid a potential data race. */
>
> Meh, I'm confused. Aren't we disabling the non-atomic path?
With this discussion it seems to me that the _IO_FLAGS2_NEED_LOCK set on
flockfile is indeed superfluous. On the code:
{
flockfile (arq);
write (STDERR_FILENO, "(1)\n", sizeof ("(1)\n"));
feof (arq);
funlockfile (arq);
}
{
write (STDERR_FILENO, "(2)\n", sizeof ("(2)\n"));
feof (arq);
}
The second feof will always take the lock because _IO_FLAGS2_NEED_LOCK is
sticky, even though this is fine to use the fast non-atomic path (I am
assuming single-thread here).
>
>>> Why not? The single-thread optimization would still avoid the atomic
>>> compare-and-set, which should be a significant win on AArch64 and POWER.
>>> There is more work to be done for the single-threaded case compared to
>>> _IO_FLAGS2_NEED_LOCK, but all that is without atomics, so I think those
>>> should be reasonably fast.
>>
>> Because it would solve only for single-thread case, for multithread
>> case the single-thread optimization won't give the function that
>> call _IO_need_lock the correct information to elide the lock.
>>
>> The seqeuence:
>>
>> flockfile (arq);
>>
>> feof (arq);
>> ferror (arq);
>> ...
>>
>> funlockfile (arq);
>>
>> Should only incur is atomic operation on flockfile and funlockfile
>> in both single and multi-thread case. BY tying the feof taking the
>> lock to single-thread optimization it incur in extra atomic operations
>> on multi-thread case.
>
> There are two relevant locking optimizations: flockfile acquires the
> lock for the current thread. At that point, the ownership check for the
> recursive lock succeeds. That check doesn't use an atomic load, so it's
> fast.
>
> The other optimization is generally enabled via __fsetlocking (arq,
> FSETLOCKING_BYCALLER), which enables the _IO_USER_LOCK flag that
> disables locking in feof and ferror as well (but not for flockfile and
> funlockfile).
>
> The latter is perhaps not necessary because the application could just
> call flockfile.
This is indeed messy, ideally I think we would like something like:
int
_IO_feof (FILE *fp)
{
__flockfile (fp);
int result = _IO_feof_unlocked ();
__funlockfile (fp);
return result;
}
void
__flockfile (FILE *stream)
{
_IO_lock_lock (*stream->lock);
}
static inline void
_IO_lock_lock (_IO_lock_t *lock)
{
void *self = THREAD_SELF;
if (lock->owner != self)
{
lll_lock (name->lock, LLL_PRIVATE);
lock->owner = self;
}
++lock->cnt;
}
The __flockfile should provide single-thread optimization through the
_IO_lock_lock (and the recursive lock). However we can't really use
__flockfile internally because of __fsetlocking (FSETLOCKING_BYCALLER),
so we need a wrapper to check for _IO_USER_LOCK (which is provided by
the _IO_need_lock wrapper).
The _IO_need_lock and _unlocked call really seems unnecessary, at least
on Linux that already provides a recursive lock with owner.
More information about the Libc-alpha
mailing list