forked from Minki/linux
mtdchar: prevent unbounded allocation in MEMWRITE ioctl
In the mtdchar_write_ioctl() function, memdup_user() is called with its 'len' parameter set to verbatim values provided by user space via a struct mtd_write_req. Both the 'len' and 'ooblen' fields of that structure are 64-bit unsigned integers, which means the MEMWRITE ioctl can trigger unbounded kernel memory allocation requests. Fix by iterating over the buffers provided by user space in a loop, processing at most mtd->erasesize bytes in each iteration. Adopt some checks from mtd_check_oob_ops() to retain backward user space compatibility. Suggested-by: Boris Brezillon <boris.brezillon@collabora.com> Signed-off-by: Michał Kępień <kernel@kempniu.pl> Signed-off-by: Miquel Raynal <miquel.raynal@bootlin.com> Link: https://lore.kernel.org/linux-mtd/20211130113149.21848-1-kernel@kempniu.pl
This commit is contained in:
parent
dd8a2e884a
commit
6420ac0af9
@ -573,14 +573,32 @@ static int mtdchar_blkpg_ioctl(struct mtd_info *mtd,
|
||||
}
|
||||
}
|
||||
|
||||
static void adjust_oob_length(struct mtd_info *mtd, uint64_t start,
|
||||
struct mtd_oob_ops *ops)
|
||||
{
|
||||
uint32_t start_page, end_page;
|
||||
u32 oob_per_page;
|
||||
|
||||
if (ops->len == 0 || ops->ooblen == 0)
|
||||
return;
|
||||
|
||||
start_page = mtd_div_by_ws(start, mtd);
|
||||
end_page = mtd_div_by_ws(start + ops->len - 1, mtd);
|
||||
oob_per_page = mtd_oobavail(mtd, ops);
|
||||
|
||||
ops->ooblen = min_t(size_t, ops->ooblen,
|
||||
(end_page - start_page + 1) * oob_per_page);
|
||||
}
|
||||
|
||||
static int mtdchar_write_ioctl(struct mtd_info *mtd,
|
||||
struct mtd_write_req __user *argp)
|
||||
{
|
||||
struct mtd_info *master = mtd_get_master(mtd);
|
||||
struct mtd_write_req req;
|
||||
struct mtd_oob_ops ops = {};
|
||||
const void __user *usr_data, *usr_oob;
|
||||
int ret;
|
||||
uint8_t *datbuf = NULL, *oobbuf = NULL;
|
||||
size_t datbuf_len, oobbuf_len;
|
||||
int ret = 0;
|
||||
|
||||
if (copy_from_user(&req, argp, sizeof(req)))
|
||||
return -EFAULT;
|
||||
@ -590,33 +608,79 @@ static int mtdchar_write_ioctl(struct mtd_info *mtd,
|
||||
|
||||
if (!master->_write_oob)
|
||||
return -EOPNOTSUPP;
|
||||
ops.mode = req.mode;
|
||||
ops.len = (size_t)req.len;
|
||||
ops.ooblen = (size_t)req.ooblen;
|
||||
ops.ooboffs = 0;
|
||||
|
||||
if (usr_data) {
|
||||
ops.datbuf = memdup_user(usr_data, ops.len);
|
||||
if (IS_ERR(ops.datbuf))
|
||||
return PTR_ERR(ops.datbuf);
|
||||
} else {
|
||||
ops.datbuf = NULL;
|
||||
if (!usr_data)
|
||||
req.len = 0;
|
||||
|
||||
if (!usr_oob)
|
||||
req.ooblen = 0;
|
||||
|
||||
if (req.start + req.len > mtd->size)
|
||||
return -EINVAL;
|
||||
|
||||
datbuf_len = min_t(size_t, req.len, mtd->erasesize);
|
||||
if (datbuf_len > 0) {
|
||||
datbuf = kmalloc(datbuf_len, GFP_KERNEL);
|
||||
if (!datbuf)
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
if (usr_oob) {
|
||||
ops.oobbuf = memdup_user(usr_oob, ops.ooblen);
|
||||
if (IS_ERR(ops.oobbuf)) {
|
||||
kfree(ops.datbuf);
|
||||
return PTR_ERR(ops.oobbuf);
|
||||
oobbuf_len = min_t(size_t, req.ooblen, mtd->erasesize);
|
||||
if (oobbuf_len > 0) {
|
||||
oobbuf = kmalloc(oobbuf_len, GFP_KERNEL);
|
||||
if (!oobbuf) {
|
||||
kfree(datbuf);
|
||||
return -ENOMEM;
|
||||
}
|
||||
} else {
|
||||
ops.oobbuf = NULL;
|
||||
}
|
||||
|
||||
ret = mtd_write_oob(mtd, (loff_t)req.start, &ops);
|
||||
while (req.len > 0 || (!usr_data && req.ooblen > 0)) {
|
||||
struct mtd_oob_ops ops = {
|
||||
.mode = req.mode,
|
||||
.len = min_t(size_t, req.len, datbuf_len),
|
||||
.ooblen = min_t(size_t, req.ooblen, oobbuf_len),
|
||||
.datbuf = datbuf,
|
||||
.oobbuf = oobbuf,
|
||||
};
|
||||
|
||||
kfree(ops.datbuf);
|
||||
kfree(ops.oobbuf);
|
||||
/*
|
||||
* Shorten non-page-aligned, eraseblock-sized writes so that
|
||||
* the write ends on an eraseblock boundary. This is necessary
|
||||
* for adjust_oob_length() to properly handle non-page-aligned
|
||||
* writes.
|
||||
*/
|
||||
if (ops.len == mtd->erasesize)
|
||||
ops.len -= mtd_mod_by_ws(req.start + ops.len, mtd);
|
||||
|
||||
/*
|
||||
* For writes which are not OOB-only, adjust the amount of OOB
|
||||
* data written according to the number of data pages written.
|
||||
* This is necessary to prevent OOB data from being skipped
|
||||
* over in data+OOB writes requiring multiple mtd_write_oob()
|
||||
* calls to be completed.
|
||||
*/
|
||||
adjust_oob_length(mtd, req.start, &ops);
|
||||
|
||||
if (copy_from_user(datbuf, usr_data, ops.len) ||
|
||||
copy_from_user(oobbuf, usr_oob, ops.ooblen)) {
|
||||
ret = -EFAULT;
|
||||
break;
|
||||
}
|
||||
|
||||
ret = mtd_write_oob(mtd, req.start, &ops);
|
||||
if (ret)
|
||||
break;
|
||||
|
||||
req.start += ops.retlen;
|
||||
req.len -= ops.retlen;
|
||||
usr_data += ops.retlen;
|
||||
|
||||
req.ooblen -= ops.oobretlen;
|
||||
usr_oob += ops.oobretlen;
|
||||
}
|
||||
|
||||
kfree(datbuf);
|
||||
kfree(oobbuf);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user