On Thu, Nov 19, 2020 at 12:05:51AM +0100, Stefan Esser wrote:
> Am 18.11.20 um 23:14 schrieb Jessica Clarke:
> > On 18 Nov 2020, at 21:52, Stefan Esser <s...@freebsd.org> wrote:
> >> Am 18.11.20 um 22:15 schrieb Jessica Clarke:
> >>> On 18 Nov 2020, at 19:44, Stefan E??er <s...@freebsd.org> wrote:
> >>>> +                /*
> >>>> +                 * Check for some other thread already having
> >>>> +                 * set localbase - this should use atomic ops.
> >>>> +                 * The amount of memory allocated above may leak,
> >>>> +                 * if a parallel update in another thread is not
> >>>> +                 * detected and the non-NULL pointer is overwritten.
> >>>> +                 */
> >>> Why was this committed with a known racy/leaky implementation?
> >>
> >> Because the alternatives that I offered for discussion were
> >> less acceptable.
> > 
> > That has no bearing over whether this one is.
> 
> Three alternatives have been discussed:
> 
> 1) static buffer of size MAXPATHLEN
> 2) dynamically allocated buffer (as committed)
> 3) dynamically allocated buffer filled by a constructor
> 
> 1) has been rejected, since it adds 1 KB of bss to each program
> that is linked with libutil, whether it uses getlocalbase() or
> not.
> 
> 3) has been rejected since it causes 1 getenv() and 2 sysctl()
> calls when the program linked with libutil starts, independently
> of whether getlocalbase() is used or not.
> 
> 2) has been selected, since it is only called when needed and it
> does not pre-allocate a large buffer.
> 
> Which other alternative do you want to suggest?
> 
> Did you have a look at the review announced on the -current list
> and mentioned in the commit?
> 
> >>> What happens if I set the value with a sysctl and call this?
> >>
> >> You'll get the value set with sysctl, unless overridden by the
> >> environment variable. There is a window of a few nano-seconds
> >> where a thread executing in parallel on another core might be
> >> able to set the localbase variable (between the test for NULL
> >> in this function and the assignment to it). The value that will
> >> be returned by either thread will be identical, so there is no
> >> risk of corruption of the result.
> > 
> > But if I call getlocalbase, then set it via sysctl, then call
> > getlocalbase again, I see the old value. If, however, I omit the first
> > getlocalbase, I see the new value. This differs from how getenv/setenv
> > of the value work, where you always see the up-to-date value. Maybe you
> > think that's a feature, but it's something to watch out for and
> > explicitly call out in the documentation.
> 
> Actual programs call getlocalbase() exactly once and create the
> required pathes from the value returned.
> 
> It is possible to copy the value from the environment to a buffer,
> but at added complexity and introducing another race condition.
> 
> The program itself might have modified its environment and then
> use an inconsistent value when it calls getlocalbase() again
> thereafter. But why would you want to do this - it seems to be
> a complicated way lof foot-shooting to me.
> 
> I'm not a native speaker of English and not best qualified to
> clearly express this in the man-page. Feel free to commit any
> clarification or suggest an addition for me to commit.
> 
> > You also misunderstand all the subtleties of multithreading here. There
> > are no acquire/release pairs so it is entirely legal for Thread 2 to
> > read Thread 1's initialised value for localbase before the contents of
> > it are visible (i.e. the pointer is initialised but the data is
> > garbage).
> 
> Yes, and thread 2's value will be identical to the one from
> thread 1, just in a different heap location, unless there is a
> write to the sysctl variable in the kernel at just the same
> time. And you cannot protect against this race and you'll get
> either the old or new value.
> 
> > The `(volatile const char*)localbase` cast is also a complete waste of
> > time. You probably meant to write `(const char * volatile)localbase`
> > but even then that does nothing useful as the cast is too late. What
> > you really were trying to write was
> > `*(const char * volatile *)&localbase`, but you need proper atomics
> > anyway for this to be safe.
> 
> I was not sure about the correct volatile declaration, I've got
> to admit. It was in the review and I did not get any comments
> regarding that specific modifier. The goal was to enforce the
> access to the localbase pointer in memory after returning from
> the sysctl to shorten the window.
> 
> Perhaps I should just have completely ignore any multi-threading
> issues and accepted that an overlapping execution of this function
> would allocate multiple sysctl destination buffers but loose all
> references but one in the assignment to localbase.
> 
> It will not happen in practice, it just does not make sense to
> call this function more than once in a program.
> 
> >> This unlikely case may actually leak a heap allocated string
> >> of typically tens of bytes (but with negligible probability).
> >>
> >> But this really is a non-issue, since there should never be a
> >> reason to invoke this function in a multi-threaded context.
> > 
> > Why not? There could easily be code out there calling getenv in a
> > multi-threaded context so this is inadequate as a replacement. Yes it's
> > inefficient but it's perfectly legal and imaginable.
> 
> Yes, calling getenv() might occur, but getlocalbase() is generally
> called before configuration files are accessed, and the resulting
> path is saved in the program. Other uses are possible, but this is
> the recurring pattern.

This seems like a very naive assumption.  I could easily see libraries
wanting to know where localbase is and calling this completely without
knowledge of the application programmer.

> And unless the program modifies its own environment at run-time,
> all invocsations of getlocalbase() are guaranteed to return the
> same result again and again.
> 
> > Also if malloc returns NULL I would quite like that to be an error for
> > the function and not silently fall back on _PATH_LOCALBASE.
> 
> The function is specifically specified to always succeed. Else
> there is extra code required at each invocation to check for
> errors. Not being able to allocate the required buffer of at
> most 1 KB will cause every non-trivial program to fail anyway.
> 
> Returning the compiled in default value will direct the program
> to access files in /usr/local, which is an administrator controlled
> path, even on systems with non-default localbase.
> 
> Returning NULL is possible in that case, but the only ways a
> program could deal with this case is to either crash, sue the
> default path, or ignore the accesses to files in localbase.
> 
> In either case it will not operate as intended, but I consider
> directing accesses to the FreeBSD default location (or the
> modified _PATH_LOCALBASE) to be a reasonable fall-back.
> 
> But this will not happen unless provoked (by not allowing the
> program to allocate any memory from the heap, which will cause
> it to fail as soon as any other library function or start-up
> code tries to allocate heap-space).
> 
> I had put this code up for review and it has been discussed
> before (and another implementation has been reverted after
> several iterations of changes).
> 
> If there had been consensus to return NULL, I could have
> committed that - it is a trivial change and ENOMEM would have
> been set after a malloc failure.
> 
> I have written about this possibility and I had appreciated
> if comments had been made on Phabricator before the commit.

Don't mistake posting something for review with obtaining
consensus.  I glanced at the review, but it contained no use cases or
justification for the feature so it was impossible to comment on the
implementation in the time I had.  I still don't understand what you aim
to support and why (except that your implementation fails to support
things like per-jail or per-ABI localbase which both seem like things
people might want.)

-- Brooks

Attachment: signature.asc
Description: PGP signature

Reply via email to