busybox/editors/ed.c
Sören Tempel f15dfd86c4 ed: don't use memcpy with overlapping memory regions
The memcpy invocations in the subCommand function, modified by this
commit, previously used memcpy with overlapping memory regions. This is
undefined behavior. On Alpine Linux, it causes BusyBox ed to crash since
we compile BusyBox with -D_FORTIFY_SOURCE=2 and our fortify-headers
implementation catches this source of undefined behavior [0]. The issue
can only be triggered if the replacement string is the same size or
shorter than the old string.

Looking at the code, it seems to me that a memmove(3) is what was
actually intended here, this commit modifies the code accordingly.

[0]: https://gitlab.alpinelinux.org/alpine/aports/-/issues/13504

Signed-off-by: Sören Tempel <soeren+git@soeren-tempel.net>
Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
2023-01-05 16:26:49 +01:00

1029 lines
19 KiB
C

/* vi: set sw=4 ts=4: */
/*
* Copyright (c) 2002 by David I. Bell
* Permission is granted to use, distribute, or modify this source,
* provided that this copyright notice remains intact.
*
* The "ed" built-in command (much simplified)
*/
//config:config ED
//config: bool "ed (21 kb)"
//config: default y
//config: help
//config: The original 1970's Unix text editor, from the days of teletypes.
//config: Small, simple, evil. Part of SUSv3. If you're not already using
//config: this, you don't need it.
//kbuild:lib-$(CONFIG_ED) += ed.o
//applet:IF_ED(APPLET(ed, BB_DIR_BIN, BB_SUID_DROP))
//usage:#define ed_trivial_usage "[-p PROMPT] [-s] [FILE]"
//usage:#define ed_full_usage ""
#include "libbb.h"
#include "common_bufsiz.h"
typedef struct LINE {
struct LINE *next;
struct LINE *prev;
int len;
char data[1];
} LINE;
#define searchString bb_common_bufsiz1
enum {
USERSIZE = COMMON_BUFSIZE > 1024 ? 1024
: COMMON_BUFSIZE - 1, /* max line length typed in by user */
INITBUF_SIZE = 1024, /* initial buffer size */
};
struct globals {
int curNum;
int lastNum;
int bufUsed;
int bufSize;
LINE *curLine;
char *bufBase;
char *bufPtr;
char *fileName;
const char *prompt;
LINE lines;
smallint dirty;
int marks[26];
};
#define G (*ptr_to_globals)
#define curLine (G.curLine )
#define bufBase (G.bufBase )
#define bufPtr (G.bufPtr )
#define fileName (G.fileName )
#define prompt (G.prompt )
#define curNum (G.curNum )
#define lastNum (G.lastNum )
#define bufUsed (G.bufUsed )
#define bufSize (G.bufSize )
#define dirty (G.dirty )
#define lines (G.lines )
#define marks (G.marks )
#define INIT_G() do { \
setup_common_bufsiz(); \
SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
} while (0)
#define OPTION_STR "sp:"
enum {
OPT_s = (1 << 0),
};
static int bad_nums(int num1, int num2, const char *for_what)
{
if ((num1 < 1) || (num2 > lastNum) || (num1 > num2)) {
bb_error_msg("bad line range for %s", for_what);
return 1;
}
return 0;
}
/*
* Return a pointer to the specified line number.
*/
static LINE *findLine(int num)
{
LINE *lp;
int lnum;
if ((num < 1) || (num > lastNum)) {
bb_error_msg("line number %d does not exist", num);
return NULL;
}
if (curNum <= 0) {
curNum = 1;
curLine = lines.next;
}
if (num == curNum)
return curLine;
lp = curLine;
lnum = curNum;
if (num < (curNum / 2)) {
lp = lines.next;
lnum = 1;
} else if (num > ((curNum + lastNum) / 2)) {
lp = lines.prev;
lnum = lastNum;
}
while (lnum < num) {
lp = lp->next;
lnum++;
}
while (lnum > num) {
lp = lp->prev;
lnum--;
}
return lp;
}
/*
* Search a line for the specified string starting at the specified
* offset in the line. Returns the offset of the found string, or -1.
*/
static int findString(const LINE *lp, const char *str, int len, int offset)
{
int left;
const char *cp, *ncp;
cp = &lp->data[offset];
left = lp->len - offset - len;
while (left >= 0) {
ncp = memchr(cp, str[0], left + 1);
if (ncp == NULL)
return -1;
left -= (ncp - cp);
cp = ncp;
if (memcmp(cp, str, len) == 0)
return (cp - lp->data);
cp++;
left--;
}
return -1;
}
/*
* Search for a line which contains the specified string.
* If the string is "", then the previously searched for string
* is used. The currently searched for string is saved for future use.
* Returns the line number which matches, or 0 if there was no match
* with an error printed.
*/
static NOINLINE int searchLines(const char *str, int num1, int num2)
{
const LINE *lp;
int len;
if (bad_nums(num1, num2, "search"))
return 0;
if (*str == '\0') {
if (searchString[0] == '\0') {
bb_simple_error_msg("no previous search string");
return 0;
}
str = searchString;
}
if (str != searchString)
strcpy(searchString, str);
len = strlen(str);
lp = findLine(num1);
if (lp == NULL)
return 0;
while (num1 <= num2) {
if (findString(lp, str, len, 0) >= 0)
return num1;
num1++;
lp = lp->next;
}
bb_error_msg("can't find string \"%s\"", str);
return 0;
}
/*
* Parse a line number argument if it is present. This is a sum
* or difference of numbers, ".", "$", "'c", or a search string.
* Returns pointer which stopped the scan if successful
* (whether or not there was a number).
* Returns NULL if there was a parsing error, with a message output.
* Whether there was a number is returned indirectly, as is the number.
*/
static const char* getNum(const char *cp, smallint *retHaveNum, int *retNum)
{
char *endStr, str[USERSIZE];
int value, num;
smallint haveNum, minus;
value = 0;
haveNum = FALSE;
minus = 0;
while (TRUE) {
cp = skip_whitespace(cp);
switch (*cp) {
case '.':
haveNum = TRUE;
num = curNum;
cp++;
break;
case '$':
haveNum = TRUE;
num = lastNum;
cp++;
break;
case '\'':
cp++;
if ((unsigned)(*cp - 'a') >= 26) {
bb_simple_error_msg("bad mark name");
return NULL;
}
haveNum = TRUE;
num = marks[(unsigned)(*cp - 'a')];
cp++;
break;
case '/':
strcpy(str, ++cp);
endStr = strchr(str, '/');
if (endStr) {
*endStr++ = '\0';
cp += (endStr - str);
} else
cp = "";
num = searchLines(str, curNum, lastNum);
if (num == 0)
return NULL;
haveNum = TRUE;
break;
default:
if (!isdigit(*cp)) {
*retHaveNum = haveNum;
*retNum = value;
return cp;
}
num = 0;
while (isdigit(*cp))
num = num * 10 + *cp++ - '0';
haveNum = TRUE;
break;
}
value += (minus ? -num : num);
cp = skip_whitespace(cp);
switch (*cp) {
case '-':
minus = 1;
cp++;
break;
case '+':
minus = 0;
cp++;
break;
default:
*retHaveNum = haveNum;
*retNum = value;
return cp;
}
}
}
/*
* Set the current line number.
* Returns TRUE if successful.
*/
static int setCurNum(int num)
{
LINE *lp;
lp = findLine(num);
if (lp == NULL)
return FALSE;
curNum = num;
curLine = lp;
return TRUE;
}
/*
* Insert a new line with the specified text.
* The line is inserted so as to become the specified line,
* thus pushing any existing and further lines down one.
* The inserted line is also set to become the current line.
* Returns TRUE if successful.
*/
static int insertLine(int num, const char *data, int len)
{
LINE *newLp, *lp;
if ((num < 1) || (num > lastNum + 1)) {
bb_simple_error_msg("inserting at bad line number");
return FALSE;
}
newLp = xmalloc(sizeof(LINE) + len - 1);
memcpy(newLp->data, data, len);
newLp->len = len;
if (num > lastNum)
lp = &lines;
else {
lp = findLine(num);
if (lp == NULL) {
free((char *) newLp);
return FALSE;
}
}
newLp->next = lp;
newLp->prev = lp->prev;
lp->prev->next = newLp;
lp->prev = newLp;
lastNum++;
dirty = TRUE;
return setCurNum(num);
}
/*
* Add lines which are typed in by the user.
* The lines are inserted just before the specified line number.
* The lines are terminated by a line containing a single dot (ugly!),
* or by an end of file.
*/
static void addLines(int num)
{
int len;
char buf[USERSIZE + 1];
while (1) {
/* Returns:
* -1 on read errors or EOF, or on bare Ctrl-D.
* 0 on ctrl-C,
* >0 length of input string, including terminating '\n'
*/
len = read_line_input(NULL, "", buf, sizeof(buf));
if (len <= 0) {
/* Previously, ctrl-C was exiting to shell.
* Now we exit to ed prompt. Is in important? */
return;
}
if (buf[0] == '.' && buf[1] == '\n' && buf[2] == '\0')
return;
if (!insertLine(num++, buf, len))
return;
}
}
/*
* Read lines from a file at the specified line number.
* Returns TRUE if the file was successfully read.
*/
static int readLines(const char *file, int num)
{
int fd, cc;
int len;
unsigned charCount;
char *cp;
if ((num < 1) || (num > lastNum + 1)) {
bb_simple_error_msg("bad line for read");
return FALSE;
}
fd = open(file, 0);
if (fd < 0) {
bb_simple_perror_msg(file);
return FALSE;
}
bufPtr = bufBase;
bufUsed = 0;
charCount = 0;
cc = 0;
do {
cp = memchr(bufPtr, '\n', bufUsed);
if (cp) {
len = (cp - bufPtr) + 1;
if (!insertLine(num, bufPtr, len)) {
close(fd);
return FALSE;
}
bufPtr += len;
bufUsed -= len;
charCount += len;
num++;
continue;
}
if (bufPtr != bufBase) {
memcpy(bufBase, bufPtr, bufUsed);
bufPtr = bufBase + bufUsed;
}
if (bufUsed >= bufSize) {
len = (bufSize * 3) / 2;
cp = xrealloc(bufBase, len);
bufBase = cp;
bufPtr = bufBase + bufUsed;
bufSize = len;
}
cc = safe_read(fd, bufPtr, bufSize - bufUsed);
bufUsed += cc;
bufPtr = bufBase;
} while (cc > 0);
if (cc < 0) {
bb_simple_perror_msg(file);
close(fd);
return FALSE;
}
if (bufUsed) {
if (!insertLine(num, bufPtr, bufUsed)) {
close(fd);
return -1;
}
charCount += bufUsed;
}
close(fd);
/* https://pubs.opengroup.org/onlinepubs/9699919799/utilities/ed.html
* "Read Command"
* "...the number of bytes read shall be written to standard output
* in the following format:
* "%d\n", <number of bytes read>
*/
if (!(option_mask32 & OPT_s))
printf("%u\n", charCount);
return TRUE;
}
/*
* Write the specified lines out to the specified file.
* Returns TRUE if successful, or FALSE on an error with a message output.
*/
static int writeLines(const char *file, int num1, int num2)
{
LINE *lp;
int fd;
unsigned charCount;
if (bad_nums(num1, num2, "write"))
return FALSE;
charCount = 0;
fd = creat(file, 0666);
if (fd < 0) {
bb_simple_perror_msg(file);
return FALSE;
}
lp = findLine(num1);
if (lp == NULL) {
close(fd);
return FALSE;
}
while (num1++ <= num2) {
if (full_write(fd, lp->data, lp->len) != lp->len) {
bb_simple_perror_msg(file);
close(fd);
return FALSE;
}
charCount += lp->len;
lp = lp->next;
}
if (close(fd) < 0) {
bb_simple_perror_msg(file);
return FALSE;
}
/* https://pubs.opengroup.org/onlinepubs/9699919799/utilities/ed.html
* "Write Command"
* "...the number of bytes written shall be written to standard output,
* unless the -s option was specified, in the following format:
* "%d\n", <number of bytes written>
*/
if (!(option_mask32 & OPT_s))
printf("%u\n", charCount);
return TRUE;
}
/*
* Print lines in a specified range.
* The last line printed becomes the current line.
* If expandFlag is TRUE, then the line is printed specially to
* show magic characters.
*/
static int printLines(int num1, int num2, int expandFlag)
{
const LINE *lp;
const char *cp;
int ch, count;
if (bad_nums(num1, num2, "print"))
return FALSE;
lp = findLine(num1);
if (lp == NULL)
return FALSE;
while (num1 <= num2) {
if (!expandFlag) {
write(STDOUT_FILENO, lp->data, lp->len);
setCurNum(num1++);
lp = lp->next;
continue;
}
/*
* Show control characters and characters with the
* high bit set specially.
*/
cp = lp->data;
count = lp->len;
if ((count > 0) && (cp[count - 1] == '\n'))
count--;
while (count-- > 0) {
ch = (unsigned char) *cp++;
fputc_printable(ch | PRINTABLE_META, stdout);
}
fputs_stdout("$\n");
setCurNum(num1++);
lp = lp->next;
}
return TRUE;
}
/*
* Delete lines from the given range.
*/
static void deleteLines(int num1, int num2)
{
LINE *lp, *nlp, *plp;
int count;
if (bad_nums(num1, num2, "delete"))
return;
lp = findLine(num1);
if (lp == NULL)
return;
if ((curNum >= num1) && (curNum <= num2)) {
if (num2 < lastNum)
setCurNum(num2 + 1);
else if (num1 > 1)
setCurNum(num1 - 1);
else
curNum = 0;
}
count = num2 - num1 + 1;
if (curNum > num2)
curNum -= count;
lastNum -= count;
while (count-- > 0) {
nlp = lp->next;
plp = lp->prev;
plp->next = nlp;
nlp->prev = plp;
free(lp);
lp = nlp;
}
dirty = TRUE;
}
/*
* Do the substitute command.
* The current line is set to the last substitution done.
*/
static void subCommand(const char *cmd, int num1, int num2)
{
char *cp, *oldStr, *newStr, buf[USERSIZE];
int delim, oldLen, newLen, deltaLen, offset;
LINE *lp, *nlp;
int globalFlag, printFlag, didSub, needPrint;
if (bad_nums(num1, num2, "substitute"))
return;
globalFlag = FALSE;
printFlag = FALSE;
didSub = FALSE;
needPrint = FALSE;
/*
* Copy the command so we can modify it.
*/
strcpy(buf, cmd);
cp = buf;
if (isblank(*cp) || (*cp == '\0')) {
bb_simple_error_msg("bad delimiter for substitute");
return;
}
delim = *cp++;
oldStr = cp;
cp = strchr(cp, delim);
if (cp == NULL) {
bb_simple_error_msg("missing 2nd delimiter for substitute");
return;
}
*cp++ = '\0';
newStr = cp;
cp = strchr(cp, delim);
if (cp)
*cp++ = '\0';
else
cp = (char*)"";
while (*cp) switch (*cp++) {
case 'g':
globalFlag = TRUE;
break;
case 'p':
printFlag = TRUE;
break;
default:
bb_simple_error_msg("unknown option for substitute");
return;
}
if (*oldStr == '\0') {
if (searchString[0] == '\0') {
bb_simple_error_msg("no previous search string");
return;
}
oldStr = searchString;
}
if (oldStr != searchString)
strcpy(searchString, oldStr);
lp = findLine(num1);
if (lp == NULL)
return;
oldLen = strlen(oldStr);
newLen = strlen(newStr);
deltaLen = newLen - oldLen;
offset = 0;
nlp = NULL;
while (num1 <= num2) {
offset = findString(lp, oldStr, oldLen, offset);
if (offset < 0) {
if (needPrint) {
printLines(num1, num1, FALSE);
needPrint = FALSE;
}
offset = 0;
lp = lp->next;
num1++;
continue;
}
needPrint = printFlag;
didSub = TRUE;
dirty = TRUE;
/*
* If the replacement string is the same size or shorter
* than the old string, then the substitution is easy.
*/
if (deltaLen <= 0) {
memcpy(&lp->data[offset], newStr, newLen);
if (deltaLen) {
memmove(&lp->data[offset + newLen],
&lp->data[offset + oldLen],
lp->len - offset - oldLen);
lp->len += deltaLen;
}
offset += newLen;
if (globalFlag)
continue;
if (needPrint) {
printLines(num1, num1, FALSE);
needPrint = FALSE;
}
lp = lp->next;
num1++;
continue;
}
/*
* The new string is larger, so allocate a new line
* structure and use that. Link it in place of
* the old line structure.
*/
nlp = xmalloc(sizeof(LINE) + lp->len + deltaLen);
nlp->len = lp->len + deltaLen;
memcpy(nlp->data, lp->data, offset);
memcpy(&nlp->data[offset], newStr, newLen);
memcpy(&nlp->data[offset + newLen],
&lp->data[offset + oldLen],
lp->len - offset - oldLen);
nlp->next = lp->next;
nlp->prev = lp->prev;
nlp->prev->next = nlp;
nlp->next->prev = nlp;
if (curLine == lp)
curLine = nlp;
free(lp);
lp = nlp;
offset += newLen;
if (globalFlag)
continue;
if (needPrint) {
printLines(num1, num1, FALSE);
needPrint = FALSE;
}
lp = lp->next;
num1++;
}
if (!didSub)
bb_error_msg("no substitutions found for \"%s\"", oldStr);
}
/*
* Read commands until we are told to stop.
*/
static void doCommands(void)
{
while (TRUE) {
char buf[USERSIZE];
const char *cp;
int len;
int n, num1, num2;
smallint h, have1, have2;
/* Returns:
* -1 on read errors or EOF, or on bare Ctrl-D.
* 0 on ctrl-C,
* >0 length of input string, including terminating '\n'
*/
len = read_line_input(NULL, prompt, buf, sizeof(buf));
if (len <= 0)
return;
while (len && isspace(buf[--len]))
buf[len] = '\0';
if ((curNum == 0) && (lastNum > 0)) {
curNum = 1;
curLine = lines.next;
}
have1 = FALSE;
have2 = FALSE;
/* Don't pass &haveN, &numN to getNum() since this forces
* compiler to keep them on stack, not in registers,
* which is usually quite suboptimal.
* Using intermediate variables shrinks code by ~150 bytes.
*/
cp = getNum(skip_whitespace(buf), &h, &n);
if (!cp)
continue;
have1 = h;
num1 = n;
cp = skip_whitespace(cp);
if (*cp == ',') {
cp = getNum(cp + 1, &h, &n);
if (!cp)
continue;
num2 = n;
if (!have1)
num1 = 1;
if (!h)
num2 = lastNum;
have1 = TRUE;
have2 = TRUE;
}
if (!have1)
num1 = curNum;
if (!have2)
num2 = num1;
switch (*cp++) {
case 'a':
addLines(num1 + 1);
break;
case 'c':
deleteLines(num1, num2);
addLines(num1);
break;
case 'd':
deleteLines(num1, num2);
break;
case 'f':
if (*cp != '\0' && *cp != ' ') {
bb_simple_error_msg("bad file command");
break;
}
cp = skip_whitespace(cp);
if (*cp == '\0') {
if (fileName)
printf("\"%s\"\n", fileName);
else
puts("No file name");
break;
}
free(fileName);
fileName = xstrdup(cp);
break;
case 'i':
if (!have1 && lastNum == 0)
num1 = 1;
addLines(num1);
break;
case 'k':
cp = skip_whitespace(cp);
if ((unsigned)(*cp - 'a') >= 26 || cp[1]) {
bb_simple_error_msg("bad mark name");
break;
}
marks[(unsigned)(*cp - 'a')] = num2;
break;
case 'l':
printLines(num1, num2, TRUE);
break;
case 'p':
printLines(num1, num2, FALSE);
break;
case 'q':
cp = skip_whitespace(cp);
if (have1 || *cp) {
bb_simple_error_msg("bad quit command");
break;
}
if (!dirty)
return;
len = read_line_input(NULL, "Really quit? ", buf, 16);
/* read error/EOF - no way to continue */
if (len < 0)
return;
cp = skip_whitespace(buf);
if ((*cp | 0x20) == 'y') /* Y or y */
return;
break;
case 'r':
if (*cp != '\0' && *cp != ' ') {
bb_simple_error_msg("bad read command");
break;
}
cp = skip_whitespace(cp);
if (*cp == '\0') {
bb_simple_error_msg("no file name");
break;
}
if (!have1)
num1 = lastNum;
if (readLines(cp, num1 + 1))
break;
if (fileName == NULL)
fileName = xstrdup(cp);
break;
case 's':
subCommand(cp, num1, num2);
break;
case 'w':
if (*cp != '\0' && *cp != ' ') {
bb_simple_error_msg("bad write command");
break;
}
cp = skip_whitespace(cp);
if (*cp == '\0') {
cp = fileName;
if (!cp) {
bb_simple_error_msg("no file name specified");
break;
}
}
if (!have1) {
num1 = 1;
num2 = lastNum;
dirty = FALSE;
}
writeLines(cp, num1, num2);
break;
case 'z':
switch (*cp) {
case '-':
printLines(curNum - 21, curNum, FALSE);
break;
case '.':
printLines(curNum - 11, curNum + 10, FALSE);
break;
default:
printLines(curNum, curNum + 21, FALSE);
break;
}
break;
case '.':
if (have1) {
bb_simple_error_msg("no arguments allowed");
break;
}
printLines(curNum, curNum, FALSE);
break;
case '-':
if (setCurNum(curNum - 1))
printLines(curNum, curNum, FALSE);
break;
case '=':
printf("%d\n", num1);
break;
case '\0':
if (have1) {
printLines(num2, num2, FALSE);
break;
}
if (setCurNum(curNum + 1))
printLines(curNum, curNum, FALSE);
break;
default:
bb_simple_error_msg("unimplemented command");
break;
}
}
}
int ed_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
int ed_main(int argc UNUSED_PARAM, char **argv)
{
INIT_G();
bufSize = INITBUF_SIZE;
bufBase = xmalloc(bufSize);
bufPtr = bufBase;
lines.next = &lines;
lines.prev = &lines;
prompt = ""; /* no prompt by default */
getopt32(argv, OPTION_STR, &prompt);
argv += optind;
if (argv[0]) {
fileName = xstrdup(argv[0]);
if (!readLines(fileName, 1)) {
return EXIT_SUCCESS;
}
dirty = FALSE;
}
doCommands();
return EXIT_SUCCESS;
}