diff options
Diffstat (limited to 'common/state/backend_bucket_circular.c')
-rw-r--r-- | common/state/backend_bucket_circular.c | 515 |
1 files changed, 515 insertions, 0 deletions
diff --git a/common/state/backend_bucket_circular.c b/common/state/backend_bucket_circular.c new file mode 100644 index 0000000000..72e165e437 --- /dev/null +++ b/common/state/backend_bucket_circular.c @@ -0,0 +1,515 @@ +/* + * Copyright (C) 2016 Pengutronix, Markus Pargmann <mpa@pengutronix.de> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * version 2, as published by the Free Software Foundation. + * + * This program 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 General Public License for more details. + * + */ + +#include <asm-generic/ioctl.h> +#include <common.h> +#include <fcntl.h> +#include <fs.h> +#include <libfile.h> +#include <linux/kernel.h> +#include <linux/mtd/mtd-abi.h> +#include <malloc.h> +#include <mtd/mtd-peb.h> +#include <string.h> + +#include "state.h" + + +struct state_backend_storage_bucket_circular { + struct state_backend_storage_bucket bucket; + + unsigned int eraseblock; /* Which eraseblock is used */ + ssize_t writesize; /* Alignment of writes */ + ssize_t max_size; /* Maximum size of this bucket */ + + off_t write_area; /* Start of the write area (relative offset) */ + uint32_t last_written_length; /* Size of the data written in the storage */ + +#ifdef __BAREBOX__ + struct mtd_info *mtd; /* mtd info (used for io in Barebox)*/ +#else + struct mtd_info_user *mtd; + int fd; +#endif + + /* For outputs */ + struct device_d *dev; +}; + +struct state_backend_storage_bucket_circular_meta { + uint32_t magic; + uint32_t written_length; +}; + +static const uint32_t circular_magic = 0x14fa2d02; +static const uint8_t free_pattern = 0xff; + +static inline struct state_backend_storage_bucket_circular + *get_bucket_circular(struct state_backend_storage_bucket *bucket) +{ + return container_of(bucket, + struct state_backend_storage_bucket_circular, + bucket); +} + +#ifdef __BAREBOX__ +static int state_mtd_peb_read(struct state_backend_storage_bucket_circular *circ, + char *buf, int offset, int len) +{ + int ret; + + ret = mtd_peb_read(circ->mtd, buf, circ->eraseblock, offset, len); + if (ret == -EBADMSG) { + ret = mtd_peb_torture(circ->mtd, circ->eraseblock); + if (ret == -EIO) { + dev_err(circ->dev, "Tortured eraseblock failed and is marked bad now, PEB %u\n", + circ->eraseblock); + return -EIO; + } else if (ret < 0) { + dev_err(circ->dev, "Failed to torture eraseblock, %d\n", + ret); + return ret; + } + /* + * Fill with invalid data so that the next write is done + * behind this area + */ + memset(buf, 0, len); + ret = -EUCLEAN; + circ->write_area = 0; + dev_dbg(circ->dev, "PEB %u has ECC error, forcing rewrite\n", + circ->eraseblock); + } else if (ret == -EUCLEAN) { + dev_dbg(circ->dev, "PEB %u is unclean, forcing rewrite\n", + circ->eraseblock); + } else if (ret < 0) { + dev_err(circ->dev, "Failed to read PEB %u, %d\n", + circ->eraseblock, ret); + } + + return ret; +} + +static int state_mtd_peb_write(struct state_backend_storage_bucket_circular *circ, + const char *buf, int offset, int len) +{ + int ret; + + ret = mtd_peb_write(circ->mtd, buf, circ->eraseblock, offset, len); + if (ret == -EBADMSG) { + ret = mtd_peb_torture(circ->mtd, circ->eraseblock); + if (ret == -EIO) { + dev_err(circ->dev, "Tortured eraseblock failed and is marked bad now, PEB %u\n", + circ->eraseblock); + return -EIO; + } else if (ret < 0) { + dev_err(circ->dev, "Failed to torture eraseblock, %d\n", + ret); + return ret; + } + ret = -EUCLEAN; + } else if (ret < 0 && ret != -EUCLEAN) { + dev_err(circ->dev, "Failed to write PEB %u, %d\n", + circ->eraseblock, ret); + } + + return ret; +} + +static int state_mtd_peb_erase(struct state_backend_storage_bucket_circular *circ) +{ + return mtd_peb_erase(circ->mtd, circ->eraseblock); +} +#else +static int state_mtd_peb_read(struct state_backend_storage_bucket_circular *circ, + char *buf, int suboffset, int len) +{ + int ret; + off_t offset = suboffset; + struct mtd_ecc_stats stat1, stat2; + bool nostats = false; + + offset += (off_t)circ->eraseblock * circ->mtd->erasesize; + + ret = lseek(circ->fd, offset, SEEK_SET); + if (ret < 0) { + dev_err(circ->dev, "Failed to set circular read position to %lld, %d\n", + offset, ret); + return ret; + } + + dev_dbg(circ->dev, "Read state from %ld length %zd\n", offset, + len); + + ret = ioctl(circ->fd, ECCGETSTATS, &stat1); + if (ret) + nostats = true; + + ret = read_full(circ->fd, buf, len); + if (ret < 0) { + dev_err(circ->dev, "Failed to read circular storage len %zd, %d\n", + len, ret); + free(buf); + return ret; + } + + if (nostats) + return 0; + + ret = ioctl(circ->fd, ECCGETSTATS, &stat2); + if (ret) + return 0; + + if (stat2.failed - stat1.failed > 0) { + ret = -EUCLEAN; + dev_dbg(circ->dev, "PEB %u has ECC error, forcing rewrite\n", + circ->eraseblock); + } else if (stat2.corrected - stat1.corrected > 0) { + ret = -EUCLEAN; + dev_dbg(circ->dev, "PEB %u is unclean, forcing rewrite\n", + circ->eraseblock); + } + + return ret; +} + +static int state_mtd_peb_write(struct state_backend_storage_bucket_circular *circ, + const char *buf, int suboffset, int len) +{ + int ret; + off_t offset = suboffset; + + offset += circ->eraseblock * circ->mtd->erasesize; + + ret = lseek(circ->fd, offset, SEEK_SET); + if (ret < 0) { + dev_err(circ->dev, "Failed to set position for circular write %ld, %d\n", + offset, ret); + return ret; + } + + ret = write_full(circ->fd, buf, len); + if (ret < 0) { + dev_err(circ->dev, "Failed to write circular to %ld length %zd, %d\n", + offset, len, ret); + return ret; + } + + /* + * We keep the fd open, so flush is necessary. We ignore the return + * value as flush is currently not supported for mtd under linux. + */ + flush(circ->fd); + + dev_dbg(circ->dev, "Written state to offset %ld length %zd data length %zd\n", + offset, len, len); + + return 0; +} + +static int state_mtd_peb_erase(struct state_backend_storage_bucket_circular *circ) +{ + return erase(circ->fd, circ->mtd->erasesize, + (off_t)circ->eraseblock * circ->mtd->erasesize); +} +#endif + +static int state_backend_bucket_circular_read(struct state_backend_storage_bucket *bucket, + uint8_t ** buf_out, + ssize_t * len_hint) +{ + struct state_backend_storage_bucket_circular *circ = + get_bucket_circular(bucket); + ssize_t read_len; + off_t offset; + uint8_t *buf; + int ret; + + /* Storage is empty */ + if (circ->write_area == 0) + return -ENODATA; + + if (!circ->last_written_length) { + /* + * Last write did not contain length information, assuming old + * state and reading from the beginning. + */ + offset = 0; + read_len = min(circ->write_area, (off_t)(circ->max_size - + sizeof(struct state_backend_storage_bucket_circular_meta))); + circ->write_area = 0; + dev_dbg(circ->dev, "Detected old on-storage format\n"); + } else if (circ->last_written_length > circ->write_area + || !IS_ALIGNED(circ->last_written_length, circ->writesize)) { + circ->write_area = 0; + dev_err(circ->dev, "Error, invalid number of bytes written last time %d\n", + circ->last_written_length); + return -EINVAL; + } else { + /* + * Normally we read at the end of the non-free area. The length + * of the read is then what we read from the meta data + * (last_written_length) + */ + read_len = circ->last_written_length; + offset = circ->write_area - read_len; + } + + buf = xmalloc(read_len); + if (!buf) + return -ENOMEM; + + dev_dbg(circ->dev, "Read state from PEB %u global offset %ld length %zd\n", + circ->eraseblock, offset, read_len); + + ret = state_mtd_peb_read(circ, buf, offset, read_len); + if (ret < 0 && ret != -EUCLEAN) { + dev_err(circ->dev, "Failed to read circular storage len %zd, %d\n", + read_len, ret); + free(buf); + return ret; + } + + *buf_out = buf; + *len_hint = read_len - sizeof(struct state_backend_storage_bucket_circular_meta); + + return ret; +} + +static int state_backend_bucket_circular_write(struct state_backend_storage_bucket *bucket, + const uint8_t * buf, + ssize_t len) +{ + struct state_backend_storage_bucket_circular *circ = + get_bucket_circular(bucket); + off_t offset; + struct state_backend_storage_bucket_circular_meta *meta; + uint32_t written_length = ALIGN(len + sizeof(*meta), circ->writesize); + int ret; + uint8_t *write_buf; + + if (written_length > circ->max_size) { + dev_err(circ->dev, "Error, state data too big to be written, to write: %zd, writesize: %zd, length: %zd, available: %zd\n", + written_length, circ->writesize, len, circ->max_size); + return -E2BIG; + } + + /* + * We need zero initialization so that our data comparisons don't show + * random changes + */ + write_buf = xzalloc(written_length); + if (!write_buf) + return -ENOMEM; + + memcpy(write_buf, buf, len); + meta = (struct state_backend_storage_bucket_circular_meta *) + (write_buf + written_length - sizeof(*meta)); + meta->magic = circular_magic; + meta->written_length = written_length; + + if (circ->write_area + written_length >= circ->max_size) { + circ->write_area = 0; + } + /* + * If the write area is at the beginning of the page, erase it and write + * at offset 0. As we only erase right before writing there are no + * conditions where we regularly erase a block multiple times without + * writing. + */ + if (circ->write_area == 0) { + dev_dbg(circ->dev, "Erasing PEB %u\n", circ->eraseblock); + ret = state_mtd_peb_erase(circ); + if (ret) { + dev_err(circ->dev, "Failed to erase PEB %u\n", + circ->eraseblock); + goto out_free; + } + } + + offset = circ->write_area; + + /* + * Update write_area before writing. The write operation may put + * arbitrary amount of the data into the storage before failing. In this + * case we want to start after that area. + */ + circ->write_area += written_length; + + ret = state_mtd_peb_write(circ, write_buf, offset, written_length); + if (ret < 0 && ret != -EUCLEAN) { + dev_err(circ->dev, "Failed to write circular to %ld length %zd, %d\n", + offset, written_length, ret); + goto out_free; + } + + dev_dbg(circ->dev, "Written state to PEB %u offset %ld length %zd data length %zd\n", + circ->eraseblock, offset, written_length, len); + +out_free: + free(write_buf); + return ret; +} + +/** + * state_backend_bucket_circular_init - Initialize circular bucket + * @param bucket + * @return 0 on success, -errno otherwise + * + * This function searches for the beginning of the written area from the end of + * the MTD device. This way it knows where the data ends and where the free area + * starts. + */ +static int state_backend_bucket_circular_init( + struct state_backend_storage_bucket *bucket) +{ + struct state_backend_storage_bucket_circular *circ = + get_bucket_circular(bucket); + int sub_offset; + uint32_t written_length = 0; + uint8_t *buf; + + buf = xmalloc(circ->writesize); + if (!buf) + return -ENOMEM; + + for (sub_offset = circ->max_size - circ->writesize; sub_offset >= 0; + sub_offset -= circ->writesize) { + int ret; + + ret = state_mtd_peb_read(circ, buf, sub_offset, + circ->writesize); + if (ret) + return ret; + + ret = mtd_buf_all_ff(buf, circ->writesize); + if (!ret) { + struct state_backend_storage_bucket_circular_meta *meta; + + meta = (struct state_backend_storage_bucket_circular_meta *) + (buf + circ->writesize - sizeof(*meta)); + + if (meta->magic != circular_magic) + written_length = 0; + else + written_length = meta->written_length; + + break; + } + } + + circ->write_area = sub_offset + circ->writesize; + circ->last_written_length = written_length; + + free(buf); + + return 0; +} + +static void state_backend_bucket_circular_free(struct + state_backend_storage_bucket + *bucket) +{ + struct state_backend_storage_bucket_circular *circ = + get_bucket_circular(bucket); + + free(circ); +} + +#ifdef __BAREBOX__ +static int bucket_circular_is_block_bad(struct state_backend_storage_bucket_circular *circ) +{ + int ret; + + ret = mtd_peb_is_bad(circ->mtd, circ->eraseblock); + if (ret < 0) + dev_err(circ->dev, "Failed to determine whether eraseblock %u is bad, %d\n", + circ->eraseblock, ret); + + return ret; +} +#else +static int bucket_circular_is_block_bad(struct state_backend_storage_bucket_circular *circ) +{ + int ret; + loff_t offs = circ->eraseblock * circ->mtd->erasesize; + + ret = ioctl(circ->fd, MEMGETBADBLOCK, &offs); + if (ret < 0) + dev_err(circ->dev, "Failed to use ioctl to check for bad block at offset %ld, %d\n", + offs, ret); + + return ret; +} +#endif + +int state_backend_bucket_circular_create(struct device_d *dev, const char *path, + struct state_backend_storage_bucket **bucket, + unsigned int eraseblock, + ssize_t writesize, + struct mtd_info_user *mtd_uinfo, + bool lazy_init) +{ + struct state_backend_storage_bucket_circular *circ; + int ret; + + circ = xzalloc(sizeof(*circ)); + circ->eraseblock = eraseblock; + circ->writesize = writesize; + circ->max_size = mtd_uinfo->erasesize; + circ->dev = dev; + +#ifdef __BAREBOX__ + circ->mtd = mtd_uinfo->mtd; +#else + circ->mtd = xzalloc(sizeof(*mtd_uinfo)); + memcpy(circ->mtd, mtd_uinfo, sizeof(*mtd_uinfo)); + circ->fd = open(path, O_RDWR); + if (circ->fd < 0) { + pr_err("Failed to open circular bucket '%s'\n", path); + return -errno; + } +#endif + + ret = bucket_circular_is_block_bad(circ); + if (ret) { + dev_info(dev, "Not using eraseblock %u, it is marked as bad (%d)\n", + circ->eraseblock, ret); + ret = -EIO; + goto out_free; + } + + circ->bucket.read = state_backend_bucket_circular_read; + circ->bucket.write = state_backend_bucket_circular_write; + circ->bucket.free = state_backend_bucket_circular_free; + *bucket = &circ->bucket; + + if (!lazy_init) { + ret = state_backend_bucket_circular_init(*bucket); + if (ret) + goto out_free; + } else { + circ->bucket.init = state_backend_bucket_circular_init; + } + + return 0; + +out_free: +#ifndef __BAREBOX__ + close(circ->fd); +#endif + free(circ); + + return ret; +} |