Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <20190725143024.GL1506@brightrain.aerifal.cx>
Date: Thu, 25 Jul 2019 10:30:24 -0400
From: Rich Felker <dalias@...c.org>
To: musl@...ts.openwall.com
Subject: Re: [PATCH v2] glob: implement GLOB_TILDE

On Thu, Jul 25, 2019 at 12:33:33PM +0200, Ismael Luceno wrote:
> 
> On 24/Jul/2019 23:48, Rich Felker wrote:
> <...>
> > > +	char *p = *pat + 1;
> > > +	size_t i = 0;
> > > +
> > > +	char *name_end = strchrnul(p, '/');
> > > +	if (*name_end) *name_end++ = 0;
> > > +	*pat = name_end;
> > > +
> > > +	char *home = (*p || issetugid()) ? NULL : getenv("HOME");
> > 
> > I still don't see any justification for violating the semantics of ~
> > because the program is suid.
> 
> I could only find a couple of implementations which don't check for
> privileges:
> - 4.3BSD
>   + NetBSD
> - uClibc
>   + uClibc-ng
> 
> Almost everyone else does:
> - FreeBSD
>   + MidnightBSD
>   + DragonFlyBSD
>   + Android (Bionic)
> - GNU
> - Newlib
> - OpenBSD
>   + MirBSD
> - Solaris
>   + Illumos
> 
> 
> I think most users/software would expect it.

Up to now I've tried to avoid just being "the maintainer's job is to
say no", but here the maintainer's job is to say no. There are 3 (or
4, if you count the dynamic linker) places in musl where libc.secure
is used:

1. preopening /dev/null on fds 0-2 if they start closed
2. disallowing arbitrary user-provided timezone files
3. disallowing arbitrary user-provided locale files
4. disallowing LD_PRELOAD/LD_LIBRARY_PATH

Here, (1) is explicitly allowed by POSIX, and the environment with
them initially closed is deemed a non-conforming execution environment
anyway. (2) and (3) are somewhat unfortunate, but necessary in order
to prevent invoking-user-controlled state from causing undefined
behavior; I actually hope to remove (2) at some point by doing a
read-and-validate instead of mmap in the suid case but it's low
priority. (4) is obviously necessary.

There's nowhere in musl where we violate the contract of an interface
with the application in suid mode because there's some conceivable way
the application could do something dangerous/wrong with the result. I
was likewise adamant about _FORTIFY_SOURCE only catching actual
overflows/UB, not things that are just "likely misusage".

~ should produce $HOME. It's arguable that it should instead lookup
the homedir based on getlogin like glibc documents, but (1) this
contradicts the Shell Command Language definition, and (2) it's not
viable in musl to begin with since getlogin() just gives you
getenv("LOGNAME").

> > > +	if (!home) {
> > > +		struct passwd pw, *res;
> > > +		switch (*p ? getpwnam_r(p, &pw, buf, PATH_MAX, &res)
> > > +			   : getpwuid_r(getuid(), &pw, buf, PATH_MAX, &res)) {
> > 
> > Same here. I'm not actually sure what should happen if $HOME is unset,
> > but it's probably whatever the Shell Command Language specification
> > says.
> 
> All the implementations I could find fall back to getpw*.

This seems consistent with POSIX sh ~:

    If the login name is null (that is, the tilde-prefix contains only
    the tilde), the tilde-prefix is replaced by the value of the
    variable HOME. If HOME is unset, the results are unspecified.
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

I'm not sure if it's the best behavior but I don't have any strong
opinion on it.

> > > +		default:
> > > +			return GLOB_ABORTED;
> > > +		case ERANGE: case ENOMEM:
> > > +			return GLOB_NOSPACE;
> > > +		case 0:
> > > +			if (!res) return GLOB_NOMATCH;
> > 
> > The man page says:
> > 
> >     If the username is invalid, or the home directory cannot be
> >     determined, then no substitution is performed.
> > 
> > and the glibc manual says:
> > 
> >     If the user name is not valid or the home directory cannot be
> >     determined for some reason the pattern is left untouched and
> >     itself used as the result
> >
> > So I think this should not be an error. I'm not entirely clear if it's
> > supposed to suppress all further glob expansion and just return a
> > single literal result, or treat "~" or "~nosuchuser" as a literal
> > component and search it.
> 
> It's supposed to continue; but IMO that's neither intuitive nor safe.
> 
> I guess that's why glibc added GLOB_TILDE_CHECK, but it should be the
> only possibility.
> 
> However, if you feel strongly about it, I can add _CHECK instead.

At first I thought I'd prefer that, and still might, but POSIX doesn't
seem to mandate anything here for the shell:

    If the system does not recognize the login name, the results are
    undefined.

But in any case we should probably define and support both flags, even
if both have the same behavior. This way applications that care will
continue to get the "check" behavior if in the future there turns out
to be reason to make GLOB_TILDE do the "non-check" thing. This will
also improve glibc ABI-compat in case a caller is using the "check"
version of the flag. (BTW I didn't check, but I assume your flag value
matches the glibc one?)

> I think:
> - ENOMEM should map to NOSPACE.
> - a closer look reveals that all other errors should map to NOMATCH.

None of the three error codes are really good, but NOMATCH is probably
best. GLOB_ABORTED may be more appropriate with GLOB_ERR set (to allow
application to distinguish between an error processing the glob and
simple nonexistence of matches). Thoughts?

> > > +		}
> > > +		home = pw.pw_dir;
> > > +	}
> > > +	while (i < PATH_MAX - 2 && *home)
> > > +		buf[i++] = *home++;
> > 
> > This could be strnlen+memmove, but perhaps just open coding it like
> > you've done is simpler and lighter.
> > 
> > > +	if (i > PATH_MAX - 2)
> > 
> > Off-by-one. If it stopped due to reaching the limit in the above loop,
> > you'll have equality not greater-than, then overflow below. Perhaps
> > strnlen+memmove would be easier to ensure the safety of.
> 
> No overflow:
> 
> i++ => PATH_MAX - 2
> i   => PATH_MAX - 1

On the last iteration of the above loop that terminates due to length
limit, i<PATH_MAX-2, so i==PATH_MAX-3. The ++ in the loop body results
in i==PATH_MAX-2.

You're right that it's not an overflow, because you only write the '/'
and null terminator below, but the user's homedir pathname has been
silently truncated (unless you happened to hit !*home on the same
iteration). I think a test like "if (*home) error-out.." would work
here.

> > > +		return GLOB_NOSPACE;
> > 
> > GLOB_NOSPACE is for allocation errors.
> 
> Right.
> 
> > This is a no-match case, since there can be no matches for a pattern
> > PATH_MAX or longer.
> 
> It must work for "~\0".

Assuming ~ expands to something that fits.

Rich

Powered by blists - more mailing lists

Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.