diff options
author | Michał Kępień <kernel@kempniu.pl> | 2022-06-29 14:57:37 +0200 |
---|---|---|
committer | Miquel Raynal <miquel.raynal@bootlin.com> | 2022-09-21 10:38:11 +0200 |
commit | 095bb6e44eb17da2cf95dbde9c83b44664a493f5 (patch) | |
tree | f3f54682ac4432b8e7428cce2ef6cad89a01feb8 /drivers/mtd/mtdchar.c | |
parent | 7bea6056927727f98f4efdd338f112f7517f05b5 (diff) | |
download | linux-095bb6e44eb17da2cf95dbde9c83b44664a493f5.tar.gz linux-095bb6e44eb17da2cf95dbde9c83b44664a493f5.tar.bz2 linux-095bb6e44eb17da2cf95dbde9c83b44664a493f5.zip |
mtdchar: add MEMREAD ioctl
User-space applications making use of MTD devices via /dev/mtd*
character devices currently have limited capabilities for reading data:
- only deprecated methods of accessing OOB layout information exist,
- there is no way to explicitly specify MTD operation mode to use; it
is auto-selected based on the MTD file mode (MTD_FILE_MODE_*) set
for the character device; in particular, this prevents using
MTD_OPS_AUTO_OOB for reads,
- all existing user-space interfaces which cause mtd_read() or
mtd_read_oob() to be called (via mtdchar_read() and
mtdchar_read_oob(), respectively) return success even when those
functions return -EUCLEAN or -EBADMSG; this renders user-space
applications using these interfaces unaware of any corrected
bitflips or uncorrectable ECC errors detected during reads.
Note that the existing MEMWRITE ioctl allows the MTD operation mode to
be explicitly set, allowing user-space applications to write page data
and OOB data without requiring them to know anything about the OOB
layout of the MTD device they are writing to (MTD_OPS_AUTO_OOB). Also,
the MEMWRITE ioctl does not mangle the return value of mtd_write_oob().
Add a new ioctl, MEMREAD, which addresses the above issues. It is
intended to be a read-side counterpart of the existing MEMWRITE ioctl.
Similarly to the latter, the read operation is performed in a loop which
processes at most mtd->erasesize bytes in each iteration. This is done
to prevent unbounded memory allocations caused by calling kmalloc() with
the 'size' argument taken directly from the struct mtd_read_req provided
by user space. However, the new ioctl is implemented so that the values
it returns match those that would have been returned if just a single
mtd_read_oob() call was issued to handle the entire read operation in
one go.
Note that while just returning -EUCLEAN or -EBADMSG to user space would
already be a valid and useful indication of the ECC algorithm detecting
errors during a read operation, that signal would not be granular enough
to cover all use cases. For example, knowing the maximum number of
bitflips detected in a single ECC step during a read operation performed
on a given page may be useful when dealing with an MTD partition whose
ECC layout varies across pages (e.g. a partition consisting of a
bootloader area using a "custom" ECC layout followed by data pages using
a "standard" ECC layout). To address that, include ECC statistics in
the structure returned to user space by the new MEMREAD ioctl.
Link: https://www.infradead.org/pipermail/linux-mtd/2016-April/067085.html
Suggested-by: Boris Brezillon <boris.brezillon@collabora.com>
Signed-off-by: Michał Kępień <kernel@kempniu.pl>
Acked-by: Richard Weinberger <richard@nod.at>
Signed-off-by: Miquel Raynal <miquel.raynal@bootlin.com>
Link: https://lore.kernel.org/linux-mtd/20220629125737.14418-5-kernel@kempniu.pl
Diffstat (limited to 'drivers/mtd/mtdchar.c')
-rw-r--r-- | drivers/mtd/mtdchar.c | 139 |
1 files changed, 139 insertions, 0 deletions
diff --git a/drivers/mtd/mtdchar.c b/drivers/mtd/mtdchar.c index 05860288a7af..01f1c6792df9 100644 --- a/drivers/mtd/mtdchar.c +++ b/drivers/mtd/mtdchar.c @@ -688,6 +688,137 @@ static int mtdchar_write_ioctl(struct mtd_info *mtd, return ret; } +static int mtdchar_read_ioctl(struct mtd_info *mtd, + struct mtd_read_req __user *argp) +{ + struct mtd_info *master = mtd_get_master(mtd); + struct mtd_read_req req; + void __user *usr_data, *usr_oob; + uint8_t *datbuf = NULL, *oobbuf = NULL; + size_t datbuf_len, oobbuf_len; + size_t orig_len, orig_ooblen; + int ret = 0; + + if (copy_from_user(&req, argp, sizeof(req))) + return -EFAULT; + + orig_len = req.len; + orig_ooblen = req.ooblen; + + usr_data = (void __user *)(uintptr_t)req.usr_data; + usr_oob = (void __user *)(uintptr_t)req.usr_oob; + + if (!master->_read_oob) + return -EOPNOTSUPP; + + if (!usr_data) + req.len = 0; + + if (!usr_oob) + req.ooblen = 0; + + req.ecc_stats.uncorrectable_errors = 0; + req.ecc_stats.corrected_bitflips = 0; + req.ecc_stats.max_bitflips = 0; + + req.len &= 0xffffffff; + req.ooblen &= 0xffffffff; + + if (req.start + req.len > mtd->size) { + ret = -EINVAL; + goto out; + } + + datbuf_len = min_t(size_t, req.len, mtd->erasesize); + if (datbuf_len > 0) { + datbuf = kvmalloc(datbuf_len, GFP_KERNEL); + if (!datbuf) { + ret = -ENOMEM; + goto out; + } + } + + oobbuf_len = min_t(size_t, req.ooblen, mtd->erasesize); + if (oobbuf_len > 0) { + oobbuf = kvmalloc(oobbuf_len, GFP_KERNEL); + if (!oobbuf) { + ret = -ENOMEM; + goto out; + } + } + + while (req.len > 0 || (!usr_data && req.ooblen > 0)) { + struct mtd_req_stats stats; + 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, + .stats = &stats, + }; + + /* + * Shorten non-page-aligned, eraseblock-sized reads so that the + * read ends on an eraseblock boundary. This is necessary in + * order to prevent OOB data for some pages from being + * duplicated in the output of non-page-aligned reads requiring + * multiple mtd_read_oob() calls to be completed. + */ + if (ops.len == mtd->erasesize) + ops.len -= mtd_mod_by_ws(req.start + ops.len, mtd); + + ret = mtd_read_oob(mtd, (loff_t)req.start, &ops); + + req.ecc_stats.uncorrectable_errors += + stats.uncorrectable_errors; + req.ecc_stats.corrected_bitflips += stats.corrected_bitflips; + req.ecc_stats.max_bitflips = + max(req.ecc_stats.max_bitflips, stats.max_bitflips); + + if (ret && !mtd_is_bitflip_or_eccerr(ret)) + break; + + if (copy_to_user(usr_data, ops.datbuf, ops.retlen) || + copy_to_user(usr_oob, ops.oobbuf, ops.oobretlen)) { + ret = -EFAULT; + break; + } + + req.start += ops.retlen; + req.len -= ops.retlen; + usr_data += ops.retlen; + + req.ooblen -= ops.oobretlen; + usr_oob += ops.oobretlen; + } + + /* + * As multiple iterations of the above loop (and therefore multiple + * mtd_read_oob() calls) may be necessary to complete the read request, + * adjust the final return code to ensure it accounts for all detected + * ECC errors. + */ + if (!ret || mtd_is_bitflip(ret)) { + if (req.ecc_stats.uncorrectable_errors > 0) + ret = -EBADMSG; + else if (req.ecc_stats.corrected_bitflips > 0) + ret = -EUCLEAN; + } + +out: + req.len = orig_len - req.len; + req.ooblen = orig_ooblen - req.ooblen; + + if (copy_to_user(argp, &req, sizeof(req))) + ret = -EFAULT; + + kvfree(datbuf); + kvfree(oobbuf); + + return ret; +} + static int mtdchar_ioctl(struct file *file, u_int cmd, u_long arg) { struct mtd_file_info *mfi = file->private_data; @@ -710,6 +841,7 @@ static int mtdchar_ioctl(struct file *file, u_int cmd, u_long arg) case MEMGETINFO: case MEMREADOOB: case MEMREADOOB64: + case MEMREAD: case MEMISLOCKED: case MEMGETOOBSEL: case MEMGETBADBLOCK: @@ -884,6 +1016,13 @@ static int mtdchar_ioctl(struct file *file, u_int cmd, u_long arg) break; } + case MEMREAD: + { + ret = mtdchar_read_ioctl(mtd, + (struct mtd_read_req __user *)arg); + break; + } + case MEMLOCK: { struct erase_info_user einfo; |