Files
Denys Vlasenko 5eee8017a8 ash: do not read past the text of an unset MAIL
Patch by Florian Schmaus <florian.schmaus@codasip.com>:

ash stores each variable as a single "NAME=value" string in var_text,
and accessor macros like mailval() simply skip over the "NAME=" prefix
(var_text + 5). Per the comment above them, they return the null
string for unset variables. That only works if an unset variable's
text still has a NUL where the value would start.

MAIL and MAILPATH are unset by default, and their initial texts are
"MAIL" and "MAILPATH". For "MAIL" (5 bytes including the NUL)
mailval() therefore points one byte past the end of the string. When
MAILPATH is not set, chkmail() falls back to mailval() and hands that
pointer to padvance_magic(), which parses the memory following "MAIL"
as a colon-separated list of mail files. This happens before every
interactive prompt, unless the user sets MAIL or MAILPATH.

What gets read there depends on the binary layout. In an x86_64
defconfig build "MAIL" is directly followed by "MAILPATH", so ash
stat()s ./MAILPATH before every prompt and prints "you have mail"
when a file of that name in the current directory changes. This
stat() of ./MAILPATH was already noticed in 2016 [1], and the
overread itself was recently reported after fuzzing ash with
AddressSanitizer [2].

The texts used to be "MAIL\0" and "MAILPATH\0", as they still are in
dash. Commit 8837c5dec4 ("ash: remove one redundant function,
improve string sharing, better field names") dropped the extra NUL
from all unset-by-default variables, so the literals become plain
strings the compiler can put in mergeable string sections. That is
fine for RANDOM, LC_ALL, LC_CTYPE and HISTFILE, whose values are only
read through lookupvar(), which checks VUNSET. But mailval() is
used without such a check.

Fix the out-of-bounds read by restoring the trailing NULs for MAIL
and MAILPATH, so that mailval() and mpathval() again yield "" for an
unset variable. This matches setvar(), which already allocates a
zeroed buffer of name length + 2 when unsetting a variable at
runtime. MAILPATH is not strictly affected, since mpathval() is only
used after mpathset(), but this keeps both accessors consistent.
Neither literal is shared with any other string, so the cost is two
bytes of .rodata:

function                                             old     new   delta
.rodata                                           102098  102100      +2

We ran into this on a CHERI [3, 4] system. CHERI's architectural
capabilities bound each memory region precisely, so it catches
out-of-bounds accesses that MMU-based protection misses. Here the
one-byte overread of the "MAIL" string literal is a capability length
violation, and every interactive ash dies with SIGSEGV on start-up.

1: https://github.com/gliderlabs/docker-alpine/issues/163
2: https://github.com/vda-linux/busybox_mirror/issues/47
3: https://www.cl.cam.ac.uk/research/security/ctsrd/cheri/
4: https://codasip.com/solutions/riscv-processor-safety-security/cheri/

Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
2026-10-22 07:31:22 +02:00
..
2009-03-19 23:09:58 +00:00
2023-07-10 17:25:21 +02:00
2022-05-01 17:06:00 +02:00
2018-07-17 15:04:17 +02:00
2010-05-20 12:56:14 +02:00

http://www.opengroup.org/onlinepubs/9699919799/
Open Group Base Specifications Issue 7


http://www.opengroup.org/onlinepubs/9699919799/utilities/V3_chap01.html
Shell & Utilities

It says that any of the standard utilities may be implemented
as a regular shell built-in. It gives a list of utilities which
are usually implemented that way (and some of them can only
be implemented as built-ins, like "alias"):

alias
bg
cd
command
false
fc
fg
getopts
jobs
kill
newgrp
pwd
read
true
umask
unalias
wait


http://www.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
Shell Command Language

It says that shell must implement special built-ins. Special built-ins
differ from regular ones by the fact that variable assignments
done on special builtin are *PRESERVED*. That is,

VAR=VAL special_builtin; echo $VAR

should print VAL.

(Another distinction is that an error in special built-in should
abort the shell, but this is not such a critical difference,
and moreover, at least bash's "set" does not follow this rule,
which is even codified in autoconf configure logic now...)

List of special builtins:

. file
: [argument...]
break [n]
continue [n]
eval [argument...]
exec [command [argument...]]
exit [n]
export name[=word]...
export -p
readonly name[=word]...
readonly -p
return [n]
set [-abCefhmnuvx] [-o option] [argument...]
set [+abCefhmnuvx] [+o option] [argument...]
set -- [argument...]
set -o
set +o
shift [n]
times
trap n [condition...]
trap [action condition...]
unset [-fv] name...

In practice, no one uses this obscure feature - none of these builtins
gives any special reasons to play such dirty tricks.

However. This section also says that *function invocation* should act
similar to special built-in. That is, variable assignments
done on function invocation should be preserved after function invocation.

This is significant: it is not unthinkable to want to run a function
with some variables set to special values. But because of the above,
it does not work: variable will "leak" out of the function.