summaryrefslogtreecommitdiffstats
path: root/common/state/backend_bucket_circular.c
diff options
context:
space:
mode:
Diffstat (limited to 'common/state/backend_bucket_circular.c')
-rw-r--r--common/state/backend_bucket_circular.c515
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;
+}