[BUG] dash -c 'echo test > "${1%.in}"' sh /tmp/META.in segfaults on master

Zurab Kvachadze <[email protected]> Tue, 29 Apr 2025 20:26:46 +0000
Newsgroups org.kernel.vger.dash
Message-ID <zrznukw6p7xhwg7tpp6cnzeqyhcg3rdp2ls6ffnbdtejtnksj5@tsxkbmidd6qx>
After a Git bisection and thorough debugging I found the bad commit to be
c5bf9702ea110bede687b57c0b5fa3fd0e15829e (expand: Add multi-byte support to
pmatch).

The command line that causes the SEGFAULT is
     
     dash -c 'echo test > "${1%.in}"' sh /tmp/META.in

I tested dash from the aforementioned commit with this command line.

The bug is "fixed" if the macro HAVE_FNMATCH is defined. The macro is controlled
by a default-disabled configure '--enable-fnmatch' switch.

The bug is in the src/expand.c::scanright() function (scanleft() seems to be
alright). Essentially, the isssue it that the function returns a pointer to
string A, but later in subevalvar() that pointer is subtracted from the base of
string B. This produces a negative integer which is later happily passed as a
parameter to memmove:

     // startp and rmesc are both char *. scan is a pointer to scanright or
     // scanleft depending on which side the text is trimmed from. In this bug,
     // scan points to scanright().
     loc = scan(startp, endp, rmesc, rmescend, str, quotes, zero);

     // quotes == 0, zero != 0
	if (!loc) {
		if (quotes) {
			rmesc = startp;
			rmescend = endp;
		}
	} else if (!quotes) {
		if (zero)
			rmesc = loc;
		else
             // Control flow goes here: rmescend now points to a substring 
             // of startp.
			rmescend = loc;
	} else if (zero) {
		rmesc = loc;
		rmescend = endp;
	} else {
		rmesc = startp;
		rmescend = loc;
	}

     // Due to the way things are allocated in dahs, startp < rmesc, thus
     // loc - rmesc evaluates to a negative integer.
     //
     // This overwrites a big chunk of memory including, the first argument 
     // of expredir(). This is where the SEGFAULT has been reported.
     // The overwrite causes the function dereference an invalid memory address
     // on the next iteration of for loop.
	memmove(startp, rmesc, rmescend - rmesc /* this is < 0 */);

(Comments have been added by me).

scanright() function look for a needle in a haystack, either from the start of
from the end of the haystack. The function receives two pairs of char pointers
(start + end), i.e. two haystacks of almost identical content, {start,end}p and
rmesc{,end}. Apart from two other non-essential parameters, the function also
receives str, the needle.

The first pair of haystack parameters specifies a pattern which can contain some
special escapes and rmesc points to string with these escapes removed:

     startp: "\201/tmp\201/META.in"
     rmesc:  "/tmp/META.in"

If FNMATCH_IS_ENABLED does not evaluate to zero (FNMATCH_IS_ENABLED depends on
HAVE_FNMATCH, i.e. FNMATCH_IS_ENABLED = 0 by default), the function iterates
over startp and return the substring of startp, not rmesc. But the calling
subevalvar() assumes the the returned value is a substring of rmesc, not startp:

     startp: 0x55555557c9b8 "\201/tmp\201/META.in"
                                              ^
             0x55555557c9c3   pointer returned from scanright()

     rmesc:  0x55555557c9cb "/tmp/META.in"
                                      ^
             0x55555557c9d4    expected pointer

subevalvar() calculates the number of bytes to transfer by (in this case)
subtracting the returned pointer loc from rmesc: 

     0x55555557c9c3 - 0x55555557c9cb

This evaluates to -8 (you can check that yourself) and, when passed as a third
argument to memmove below, we have our good ol' buffer overwrite:

=================================================================
==8694==ERROR: AddressSanitizer: negative-size-param: (size=-8)
     #0 0x7f316ed3b9ed in memmove (/usr/lib/gcc/x86_64-pc-linux-gnu/14/libasan.so.8+0x13b9ed)
     #1 0x5617237b53f4 in subevalvar (/home/master/dev/dash/build/src/dash+0x213f4)
     #2 0x5617237b57a1 in evalvar (/home/master/dev/dash/build/src/dash+0x217a1)
     #3 0x5617237b3c42 in argstr (/home/master/dev/dash/build/src/dash+0x1fc42)
     #4 0x5617237b35e0 in expandarg (/home/master/dev/dash/build/src/dash+0x1f5e0)
     #5 0x5617237abb92 in expredir (/home/master/dev/dash/build/src/dash+0x17b92)
     #6 0x5617237ad944 in evalcommand (/home/master/dev/dash/build/src/dash+0x19944)
     #7 0x5617237aa910 in evaltree (/home/master/dev/dash/build/src/dash+0x16910)
     #8 0x5617237aa236 in evalstring (/home/master/dev/dash/build/src/dash+0x16236)
     #9 0x5617237c4d86 in main (/home/master/dev/dash/build/src/dash+0x30d86)
     #10 0x7f316ea345cd  (/usr/lib64/libc.so.6+0x265cd)
     #11 0x7f316ea34688 in __libc_start_main (/usr/lib64/libc.so.6+0x26688)
     #12 0x5617237a5e94 in _start (/home/master/dev/dash/build/src/dash+0x11e94)

0x56172380022b is located 331 bytes inside of global variable 'stackbase' defined in '../../src/memalloc.c:110:20' (0x5617238000e0) of size 512
SUMMARY: AddressSanitizer: negative-size-param (/usr/lib/gcc/x86_64-pc-linux-gnu/14/libasan.so.8+0x13b9ed) in memmove
==8694==ABORTING


Currently, I am working on a patch. Should come up with something soon-ish.

-- 
Zurab Kvachadze