procps/lib/procio.c
Patrick Steinhardt 09a3687547 procio: fix potential out-of-bounds access when write fails
When writing to procfs via `proc_write` fails, we try to chunk the
buffer into smaller pieces to work around that issue. When searching for
the next location to split the buffer, though, we can underflow the
buffer in case the current offset is smaller than `LINELEN`. Fix the
issue by passing `cookie->offset` instead of `LINELEN` into `memrchr` in
case `cookie->offset` is smaller than `LINELEN`.

This bug can be triggered on musl-based systems, e.g. by executing

    $ sysctl kernel.printk_ratelimit=1000000000000000

As the value is out-of-range, `write` will return an error and set
`errno` to `EINVAL`. As we're only trying to write a smallish buffer
with a length smaller than `LINELEN` and as the buffer does not contain
any newlines, the call

    token = (char*)memrchr(cookie->buf+offset, '\n', LINELEN);

will underflow the buffer and crash the program.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
2019-03-04 21:55:07 +11:00

293 lines
5.8 KiB
C

/*
* procio.c -- Replace stdio for read and write on files below
* proc to be able to read and write large buffers as well.
*
* Copyright (C) 2017 Werner Fink
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef _GNU_SOURCE
# define _GNU_SOURCE
#endif
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct pcookie {
char *buf;
size_t count;
size_t length;
off_t offset;
int fd;
int delim;
int final:1;
} pcookie_t;
static ssize_t proc_read(void *, char *, size_t);
static ssize_t proc_write(void *, const char *, size_t);
static int proc_close(void *);
__extension__
static cookie_io_functions_t procio = {
.read = proc_read,
.write = proc_write,
.seek = NULL,
.close = proc_close,
};
FILE *fprocopen(const char *path, const char *mode)
{
pcookie_t *cookie = NULL;
FILE *handle = NULL;
mode_t flags = 0;
size_t len = 0;
int c, delim;
if (!mode || !(len = strlen(mode))) {
errno = EINVAL;
goto out;
}
/* No append mode possible */
switch (mode[0]) {
case 'r':
flags |= O_RDONLY;
break;
case 'w':
flags |= O_WRONLY|O_TRUNC;
break;
default:
errno = EINVAL;
goto out;
}
delim = ','; /* default delimeter is the comma */
for (c = 1; c < len; c++) {
switch (mode[c]) {
case '\0':
break;
case '+':
errno = EINVAL;
goto out;
case 'e':
flags |= O_CLOEXEC;
continue;
case 'b':
case 'm':
case 'x':
/* ignore this */
continue;
default:
if (mode[c] == ' ' || (mode[c] >= ',' && mode[c] <= '.') || mode[c] == ':')
delim = mode[c];
else {
errno = EINVAL;
goto out;
}
break;
}
break;
}
cookie = (pcookie_t *)malloc(sizeof(pcookie_t));
if (!cookie)
goto out;
cookie->count = BUFSIZ;
cookie->buf = (char *)malloc(cookie->count);
if (!cookie->buf) {
int errsv = errno;
free(cookie);
errno = errsv;
goto out;
}
cookie->final = 0;
cookie->offset = 0;
cookie->length = 0;
cookie->delim = delim;
cookie->fd = openat(AT_FDCWD, path, flags);
if (cookie->fd < 0) {
int errsv = errno;
free(cookie->buf);
free(cookie);
errno = errsv;
goto out;
}
handle = fopencookie(cookie, mode, procio);
if (!handle) {
int errsv = errno;
close(cookie->fd);
free(cookie->buf);
free(cookie);
errno = errsv;
goto out;
}
out:
return handle;
}
static
ssize_t proc_read(void *c, char *buf, size_t count)
{
pcookie_t *cookie = c;
ssize_t len = -1;
void *ptr;
if (cookie->count < count) {
ptr = realloc(cookie->buf, count);
if (!ptr)
goto out;
cookie->buf = ptr;
cookie->count = count;
}
while (!cookie->final) {
len = read(cookie->fd, cookie->buf, cookie->count);
if (len <= 0) {
if (len == 0) {
/* EOF */
cookie->final = 1;
cookie->buf[cookie->length] = '\0';
break;
}
goto out; /* error or done */
}
cookie->length = len;
if (cookie->length < cookie->count)
continue;
/* Likly to small buffer here */
lseek(cookie->fd, 0, SEEK_SET); /* reset for a retry */
ptr = realloc(cookie->buf, cookie->count += BUFSIZ);
if (!ptr)
goto out;
cookie->buf = ptr;
}
len = count;
if (cookie->length - cookie->offset < len)
len = cookie->length - cookie->offset;
if (len < 0)
len = 0;
if (len) {
(void)memcpy(buf, cookie->buf+cookie->offset, len);
cookie->offset += len;
} else
len = EOF;
out:
return len;
}
#define LINELEN 4096
static
ssize_t proc_write(void *c, const char *buf, size_t count)
{
pcookie_t *cookie = c;
ssize_t len = -1;
void *ptr;
if (!count) {
len = 0;
goto out;
}
/* NL is the final input */
cookie->final = memrchr(buf, '\n', count) ? 1 : 0;
while (cookie->count < cookie->offset + count) {
ptr = realloc(cookie->buf, cookie->count += count);
if (!ptr)
goto out;
cookie->buf = ptr;
}
len = count;
(void)memcpy(cookie->buf+cookie->offset, buf, count);
cookie->offset += count;
if (cookie->final) {
len = write(cookie->fd, cookie->buf, cookie->offset);
if (len < 0 && errno == EINVAL) {
size_t offset;
off_t amount;
char *token;
/*
* Oops buffer might be to large, split buffer into
* pieces at delimeter if provided
*/
if (!cookie->delim)
goto out; /* Hey dude?! */
offset = 0;
do {
token = NULL;
if (cookie->offset > LINELEN)
token = (char*)memrchr(cookie->buf+offset, cookie->delim, LINELEN);
else
token = (char*)memrchr(cookie->buf+offset, '\n', cookie->offset);
if (token)
*token = '\n';
else {
errno = EINVAL;
len = -1;
goto out; /* Wrong/Missing delimeter? */
}
if (offset > 0)
lseek(cookie->fd, 1, SEEK_CUR);
amount = token-(cookie->buf+offset)+1;
ptr = cookie->buf+offset;
len = write(cookie->fd, ptr, amount);
if (len < 1 || len >= cookie->offset)
break;
offset += len;
cookie->offset -= len;
} while (cookie->offset > 0);
}
if (len > 0)
len = count;
}
out:
return len;
}
static
int proc_close(void *c)
{
pcookie_t *cookie = c;
close(cookie->fd);
free(cookie->buf);
free(cookie);
return 0;
}