generic: routerboot sysfs: add support for soft_config
authorThibaut VARÈNE <hacks@slashdirt.org>
Wed, 13 May 2020 20:12:41 +0000 (22:12 +0200)
committerKoen Vandeputte <koen.vandeputte@ncentric.com>
Thu, 28 May 2020 09:09:10 +0000 (11:09 +0200)
This driver exposes the data encoded in the "soft_config" flash segment
of MikroTik RouterBOARDs devices. It presents the data in a sysfs folder
named "soft_config" through a set of human-and-machine-parseable
attributes. Changes can be discarded by writing 0 to the 'commit'
attribute, or they can be committed to flash storage by writing 1.

This driver does not reuse any of the existing code previously found in
the "rbcfg" utility and makes this utility obsolete by providing a clean
sysfs interface.

Like "rbcfg", this driver requires 4K_SECTORS support since the flash
partition in which these parameters are stored is typically 4KB in size.

Tested-by: Koen Vandeputte <koen.vandeputte@ncentric.com>
Tested-by: Roger Pueyo Centelles <roger.pueyo@guifi.net>
Signed-off-by: Thibaut VARÈNE <hacks@slashdirt.org>
target/linux/generic/files/drivers/platform/mikrotik/Kconfig
target/linux/generic/files/drivers/platform/mikrotik/Makefile
target/linux/generic/files/drivers/platform/mikrotik/rb_hardconfig.c
target/linux/generic/files/drivers/platform/mikrotik/rb_softconfig.c [new file with mode: 0644]
target/linux/generic/files/drivers/platform/mikrotik/routerboot.c
target/linux/generic/files/drivers/platform/mikrotik/routerboot.h

index 3a0c32604e00f47ad5e5c1761c2466e637197d97..7499ba1e1c61884f9671a5fb1abfcc5f05fdaed3 100644 (file)
@@ -12,6 +12,7 @@ config MIKROTIK_RB_SYSFS
        tristate "RouterBoot sysfs support"
        depends on MTD
        select LZO_DECOMPRESS
+       select CRC32
        help
          This driver exposes RouterBoot configuration in sysfs.
 
index 4d50ede9ff6b886532a9feb241e7aac3c49090a2..a232e1a9e84888dd13e56b27d3c751a56f8343e1 100644 (file)
@@ -1,4 +1,4 @@
 #
 # Makefile for MikroTik RouterBoard platform specific drivers
 #
-obj-$(CONFIG_MIKROTIK_RB_SYSFS)     += routerboot.o rb_hardconfig.o
+obj-$(CONFIG_MIKROTIK_RB_SYSFS)     += routerboot.o rb_hardconfig.o rb_softconfig.o
index bef71a0b5ffdfc516f273bef9172cbd15ea52daa..a03aa21b4d55ff7a54f9693c99cad2b0d95bd1d7 100644 (file)
@@ -675,6 +675,9 @@ int __init rb_hardconfig_init(struct kobject *rb_kobj)
        int i, ret;
        u32 magic;
 
+       hc_buf = NULL;
+       hc_kobj = NULL;
+
        // TODO allow override
        mtd = get_mtd_device_nm(RB_MTD_HARD_CONFIG);
        if (IS_ERR(mtd))
@@ -749,6 +752,7 @@ int __init rb_hardconfig_init(struct kobject *rb_kobj)
 
 fail:
        kfree(hc_buf);
+       hc_buf = NULL;
        return ret;
 }
 
diff --git a/target/linux/generic/files/drivers/platform/mikrotik/rb_softconfig.c b/target/linux/generic/files/drivers/platform/mikrotik/rb_softconfig.c
new file mode 100644 (file)
index 0000000..63a3b17
--- /dev/null
@@ -0,0 +1,692 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Driver for MikroTik RouterBoot soft config.
+ *
+ * Copyright (C) 2020 Thibaut VARÈNE <hacks+kernel@slashdirt.org>
+ *
+ * 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 driver exposes the data encoded in the "soft_config" flash segment of
+ * MikroTik RouterBOARDs devices. It presents the data in a sysfs folder
+ * named "soft_config". The data is presented in a user/machine-friendly way
+ * with just as much parsing as can be generalized across mikrotik platforms
+ * (as inferred from reverse-engineering).
+ *
+ * The known soft_config tags are presented in the "soft_config" sysfs folder,
+ * with the addition of one specific file named "commit", which is only
+ * available if the driver supports writes to the mtd device: no modifications
+ * made to any of the other attributes are actually written back to flash media
+ * until a true value is input into this file (e.g. [Yy1]). This is to avoid
+ * unnecessary flash wear, and to permit to revert all changes by issuing a
+ * false value ([Nn0]). Reading the content of this file shows the current
+ * status of the driver: if the data in sysfs matches the content of the
+ * soft_config partition, the file will read "clean". Otherwise, it will read
+ * "dirty".
+ *
+ * The writeable sysfs files presented by this driver will accept only inputs
+ * which are in a valid range for the given tag. As a design choice, the driver
+ * will not assess whether the inputs are identical to the existing data.
+ *
+ * Note: PAGE_SIZE is assumed to be >= 4K, hence the device attribute show
+ * routines need not check for output overflow.
+ *
+ * Some constant defines extracted from rbcfg.h by Gabor Juhos
+ * <juhosg@openwrt.org>
+ */
+
+#include <linux/types.h>
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/slab.h>
+#include <linux/errno.h>
+#include <linux/kobject.h>
+#include <linux/string.h>
+#include <linux/mtd/mtd.h>
+#include <linux/sysfs.h>
+#include <linux/version.h>
+#include <linux/capability.h>
+#include <linux/spinlock.h>
+#include <linux/crc32.h>
+
+#include "routerboot.h"
+
+#define RB_SOFTCONFIG_VER              "0.01"
+#define RB_SC_PR_PFX                   "[rb_softconfig] "
+
+/*
+ * mtd operations before 4.17 are asynchronous, not handled by this code
+ * Also make the driver act read-only if 4K_SECTORS are not enabled, since they
+ * are require to handle partial erasing of the small soft_config partition.
+ */
+#if (LINUX_VERSION_CODE >= KERNEL_VERSION(4, 17, 0)) && defined(CONFIG_MTD_SPI_NOR_USE_4K_SECTORS)
+ #define RB_SC_HAS_WRITE_SUPPORT       true
+ #define RB_SC_WMODE                   S_IWUSR
+ #define RB_SC_RMODE                   S_IRUSR
+#else
+ #define RB_SC_HAS_WRITE_SUPPORT       false
+ #define RB_SC_WMODE                   0
+ #define RB_SC_RMODE                   S_IRUSR
+#endif
+
+/* ID values for software settings */
+#define RB_SCID_UART_SPEED             0x01    // u32*1
+#define RB_SCID_BOOT_DELAY             0x02    // u32*1
+#define RB_SCID_BOOT_DEVICE            0x03    // u32*1
+#define RB_SCID_BOOT_KEY               0x04    // u32*1
+#define RB_SCID_CPU_MODE               0x05    // u32*1
+#define RB_SCID_BIOS_VERSION           0x06    // str
+#define RB_SCID_BOOT_PROTOCOL          0x09    // u32*1
+#define RB_SCID_CPU_FREQ_IDX           0x0C    // u32*1
+#define RB_SCID_BOOTER                 0x0D    // u32*1
+#define RB_SCID_SILENT_BOOT            0x0F    // u32*1
+/*
+ * protected_routerboot seems to use tag 0x1F. It only works in combination with
+ * RouterOS, resulting in a wiped board otherwise, so it's not implemented here.
+ * The tag values are as follows:
+ * - off: 0x0
+ * - on: the lower halfword encodes the max value in s for the reset feature,
+ *      the higher halfword encodes the min value in s for the reset feature.
+ * Default value when on: 0x00140258: 0x14 = 20s / 0x258= 600s
+ * See details here: https://wiki.mikrotik.com/wiki/Manual:RouterBOARD_settings#Protected_bootloader
+ */
+
+/* Tag values */
+
+#define RB_UART_SPEED_115200           0
+#define RB_UART_SPEED_57600            1
+#define RB_UART_SPEED_38400            2
+#define RB_UART_SPEED_19200            3
+#define RB_UART_SPEED_9600             4
+#define RB_UART_SPEED_4800             5
+#define RB_UART_SPEED_2400             6
+#define RB_UART_SPEED_1200             7
+#define RB_UART_SPEED_OFF              8
+
+/* valid boot delay: 1 - 9s in 1s increment */
+#define RB_BOOT_DELAY_MIN              1
+#define RB_BOOT_DELAY_MAX              9
+
+#define RB_BOOT_DEVICE_ETHER           0       // "boot over Ethernet"
+#define RB_BOOT_DEVICE_NANDETH         1       // "boot from NAND, if fail then Ethernet"
+#define RB_BOOT_DEVICE_CFCARD          2       // (not available in rbcfg)
+#define RB_BOOT_DEVICE_ETHONCE         3       // "boot Ethernet once, then NAND"
+#define RB_BOOT_DEVICE_NANDONLY                5       // "boot from NAND only"
+#define RB_BOOT_DEVICE_FLASHCFG                7       // "boot in flash configuration mode"
+#define RB_BOOT_DEVICE_FLSHONCE                8       // "boot in flash configuration mode once, then NAND"
+
+/*
+ * ATH79 CPU frequency indices.
+ * It is unknown if they apply to all ATH79 RBs, and some do not seem to feature
+ * the up levels (QCA955x), while U3 is presumably AR9344-only.
+ */
+#define RB_CPU_FREQ_IDX_ATH79_D2       (0 << 3)
+#define RB_CPU_FREQ_IDX_ATH79_D1       (1 << 3)        // 0x8
+#define RB_CPU_FREQ_IDX_ATH79_N0       (2 << 3)        // 0x10 - factory freq for many devices
+#define RB_CPU_FREQ_IDX_ATH79_U1       (3 << 3)        // 0x18
+#define RB_CPU_FREQ_IDX_ATH79_U2       (4 << 3)        // 0x20
+#define RB_CPU_FREQ_IDX_ATH79_U3       (5 << 3)        // 0x28
+
+#define RB_SC_CRC32_OFFSET             4       // located right after magic
+
+static struct kobject *sc_kobj;
+static u8 *sc_buf;
+static size_t sc_buflen;
+static rwlock_t sc_bufrwl;             // rw lock to sc_buf
+
+/* MUST be used with lock held */
+#define RB_SC_CLRCRC()         *(u32 *)(sc_buf + RB_SC_CRC32_OFFSET) = 0
+#define RB_SC_GETCRC()         *(u32 *)(sc_buf + RB_SC_CRC32_OFFSET)
+#define RB_SC_SETCRC(_crc)     *(u32 *)(sc_buf + RB_SC_CRC32_OFFSET) = (_crc)
+
+struct sc_u32tvs {
+       const u32 val;
+       const char *str;
+};
+
+#define RB_SC_TVS(_val, _str) {                \
+       .val = (_val),                  \
+       .str = (_str),                  \
+}
+
+static ssize_t sc_tag_show_u32tvs(const u8 *pld, u16 pld_len, char *buf,
+                                 const struct sc_u32tvs tvs[], const int tvselmts)
+{
+       const char *fmt;
+       char *out = buf;
+       u32 data;       // cpu-endian
+       int i;
+
+       if (sizeof(data) != pld_len)
+               return -EINVAL;
+
+       read_lock(&sc_bufrwl);
+       data = *(u32 *)pld;             // pld aliases sc_buf
+       read_unlock(&sc_bufrwl);
+
+       for (i = 0; i < tvselmts; i++) {
+               fmt = (tvs[i].val == data) ? "[%s] " : "%s ";
+               out += sprintf(out, fmt, tvs[i].str);
+       }
+
+       out += sprintf(out, "\n");
+       return out - buf;
+}
+
+static ssize_t sc_tag_store_u32tvs(const u8 *pld, u16 pld_len, const char *buf, size_t count,
+                                  const struct sc_u32tvs tvs[], const int tvselmts)
+{
+       int i;
+
+       if (sizeof(u32) != pld_len)
+               return -EINVAL;
+
+       for (i = 0; i < tvselmts; i++) {
+               if (sysfs_streq(buf, tvs[i].str)) {
+                       write_lock(&sc_bufrwl);
+                       *(u32 *)pld = tvs[i].val;       // pld aliases sc_buf
+                       RB_SC_CLRCRC();
+                       write_unlock(&sc_bufrwl);
+                       return count;
+               }
+       }
+
+       return -EINVAL;
+}
+
+struct sc_boolts {
+       const char *strfalse;
+       const char *strtrue;
+};
+
+static ssize_t sc_tag_show_boolts(const u8 *pld, u16 pld_len, char *buf,
+                                 const struct sc_boolts *bts)
+{
+       const char *fmt;
+       char *out = buf;
+       u32 data;       // cpu-endian
+
+       if (sizeof(data) != pld_len)
+               return -EINVAL;
+
+       read_lock(&sc_bufrwl);
+       data = *(u32 *)pld;             // pld aliases sc_buf
+       read_unlock(&sc_bufrwl);
+
+       fmt = (data) ? "%s [%s]\n" : "[%s] %s\n";
+       out += sprintf(out, fmt, bts->strfalse, bts->strtrue);
+
+       return out - buf;
+}
+
+static ssize_t sc_tag_store_boolts(const u8 *pld, u16 pld_len, const char *buf, size_t count,
+                                  const struct sc_boolts *bts)
+{
+       u32 data;       // cpu-endian
+
+       if (sizeof(data) != pld_len)
+               return -EINVAL;
+
+       if (sysfs_streq(buf, bts->strfalse))
+               data = 0;
+       else if (sysfs_streq(buf, bts->strtrue))
+               data = 1;
+       else
+               return -EINVAL;
+
+       write_lock(&sc_bufrwl);
+       *(u32 *)pld = data;             // pld aliases sc_buf
+       RB_SC_CLRCRC();
+       write_unlock(&sc_bufrwl);
+
+       return count;
+}
+static struct sc_u32tvs const sc_uartspeeds[] = {
+       RB_SC_TVS(RB_UART_SPEED_OFF,    "off"),
+       RB_SC_TVS(RB_UART_SPEED_1200,   "1200"),
+       RB_SC_TVS(RB_UART_SPEED_2400,   "2400"),
+       RB_SC_TVS(RB_UART_SPEED_4800,   "4800"),
+       RB_SC_TVS(RB_UART_SPEED_9600,   "9600"),
+       RB_SC_TVS(RB_UART_SPEED_19200,  "19200"),
+       RB_SC_TVS(RB_UART_SPEED_38400,  "38400"),
+       RB_SC_TVS(RB_UART_SPEED_57600,  "57600"),
+       RB_SC_TVS(RB_UART_SPEED_115200, "115200"),
+};
+
+/*
+ * While the defines are carried over from rbcfg, use strings that more clearly
+ * show the actual setting purpose (especially since the NAND* settings apply
+ * to both nand- and nor-based devices). "cfcard" was disabled in rbcfg: disable
+ * it here too.
+ */
+static struct sc_u32tvs const sc_bootdevices[] = {
+       RB_SC_TVS(RB_BOOT_DEVICE_ETHER,         "eth"),
+       RB_SC_TVS(RB_BOOT_DEVICE_NANDETH,       "flasheth"),
+       //RB_SC_TVS(RB_BOOT_DEVICE_CFCARD,      "cfcard"),
+       RB_SC_TVS(RB_BOOT_DEVICE_ETHONCE,       "ethonce"),
+       RB_SC_TVS(RB_BOOT_DEVICE_NANDONLY,      "flash"),
+       RB_SC_TVS(RB_BOOT_DEVICE_FLASHCFG,      "cfg"),
+       RB_SC_TVS(RB_BOOT_DEVICE_FLSHONCE,      "cfgonce"),
+};
+
+static struct sc_boolts const sc_bootkey = {
+       .strfalse = "any",
+       .strtrue = "del",
+};
+
+static struct sc_boolts const sc_cpumode = {
+       .strfalse = "powersave",
+       .strtrue = "regular",
+};
+
+static struct sc_boolts const sc_bootproto = {
+       .strfalse = "bootp",
+       .strtrue = "dhcp",
+};
+
+static struct sc_boolts const sc_booter = {
+       .strfalse = "regular",
+       .strtrue = "backup",
+};
+
+static struct sc_boolts const sc_silent_boot = {
+       .strfalse = "off",
+       .strtrue = "on",
+};
+
+#define SC_TAG_SHOW_STORE_U32TVS_FUNCS(_name)          \
+static ssize_t sc_tag_show_##_name(const u8 *pld, u16 pld_len, char *buf)      \
+{                                                                              \
+       return sc_tag_show_u32tvs(pld, pld_len, buf, sc_##_name, ARRAY_SIZE(sc_##_name));       \
+}                                                                              \
+static ssize_t sc_tag_store_##_name(const u8 *pld, u16 pld_len, const char *buf, size_t count) \
+{                                                                              \
+       return sc_tag_store_u32tvs(pld, pld_len, buf, count, sc_##_name, ARRAY_SIZE(sc_##_name));       \
+}
+
+#define SC_TAG_SHOW_STORE_BOOLTS_FUNCS(_name)          \
+static ssize_t sc_tag_show_##_name(const u8 *pld, u16 pld_len, char *buf)      \
+{                                                                              \
+       return sc_tag_show_boolts(pld, pld_len, buf, &sc_##_name);      \
+}                                                                              \
+static ssize_t sc_tag_store_##_name(const u8 *pld, u16 pld_len, const char *buf, size_t count) \
+{                                                                              \
+       return sc_tag_store_boolts(pld, pld_len, buf, count, &sc_##_name);      \
+}
+
+SC_TAG_SHOW_STORE_U32TVS_FUNCS(uartspeeds)
+SC_TAG_SHOW_STORE_U32TVS_FUNCS(bootdevices)
+SC_TAG_SHOW_STORE_BOOLTS_FUNCS(bootkey)
+SC_TAG_SHOW_STORE_BOOLTS_FUNCS(cpumode)
+SC_TAG_SHOW_STORE_BOOLTS_FUNCS(bootproto)
+SC_TAG_SHOW_STORE_BOOLTS_FUNCS(booter)
+SC_TAG_SHOW_STORE_BOOLTS_FUNCS(silent_boot)
+
+static ssize_t sc_tag_show_bootdelays(const u8 *pld, u16 pld_len, char *buf)
+{
+       const char *fmt;
+       char *out = buf;
+       u32 data;       // cpu-endian
+       int i;
+
+       if (sizeof(data) != pld_len)
+               return -EINVAL;
+
+       read_lock(&sc_bufrwl);
+       data = *(u32 *)pld;             // pld aliases sc_buf
+       read_unlock(&sc_bufrwl);
+
+       for (i = RB_BOOT_DELAY_MIN; i <= RB_BOOT_DELAY_MAX; i++) {
+               fmt = (i == data) ? "[%d] " : "%d ";
+               out += sprintf(out, fmt, i);
+       }
+
+       out += sprintf(out, "\n");
+       return out - buf;
+}
+
+static ssize_t sc_tag_store_bootdelays(const u8 *pld, u16 pld_len, const char *buf, size_t count)
+{
+       u32 data;       // cpu-endian
+       int ret;
+
+       if (sizeof(data) != pld_len)
+               return -EINVAL;
+
+       ret = kstrtou32(buf, 10, &data);
+       if (ret)
+               return ret;
+
+       if ((data < RB_BOOT_DELAY_MIN) || (RB_BOOT_DELAY_MAX < data))
+               return -EINVAL;
+
+       write_lock(&sc_bufrwl);
+       *(u32 *)pld = data;             // pld aliases sc_buf
+       RB_SC_CLRCRC();
+       write_unlock(&sc_bufrwl);
+
+       return count;
+}
+
+static ssize_t sc_attr_show(struct kobject *kobj, struct kobj_attribute *attr,
+                           char *buf);
+static ssize_t sc_attr_store(struct kobject *kobj, struct kobj_attribute *attr,
+                            const char *buf, size_t count);
+
+/* Array of known tags to publish in sysfs */
+static struct sc_attr {
+       const u16 tag_id;
+       /* sysfs tag show attribute. Must lock sc_buf when dereferencing pld */
+       ssize_t (* const tshow)(const u8 *pld, u16 pld_len, char *buf);
+       /* sysfs tag store attribute. Must lock sc_buf when dereferencing pld */
+       ssize_t (* const tstore)(const u8 *pld, u16 pld_len, const char *buf, size_t count);
+       struct kobj_attribute kattr;
+       u16 pld_ofs;
+       u16 pld_len;
+} sc_attrs[] = {
+       {
+               .tag_id = RB_SCID_UART_SPEED,
+               .tshow = sc_tag_show_uartspeeds,
+               .tstore = sc_tag_store_uartspeeds,
+               .kattr = __ATTR(uart_speed, RB_SC_RMODE|RB_SC_WMODE, sc_attr_show, sc_attr_store),
+       }, {
+               .tag_id = RB_SCID_BOOT_DELAY,
+               .tshow = sc_tag_show_bootdelays,
+               .tstore = sc_tag_store_bootdelays,
+               .kattr = __ATTR(boot_delay, RB_SC_RMODE|RB_SC_WMODE, sc_attr_show, sc_attr_store),
+       }, {
+               .tag_id = RB_SCID_BOOT_DEVICE,
+               .tshow = sc_tag_show_bootdevices,
+               .tstore = sc_tag_store_bootdevices,
+               .kattr = __ATTR(boot_device, RB_SC_RMODE|RB_SC_WMODE, sc_attr_show, sc_attr_store),
+       }, {
+               .tag_id = RB_SCID_BOOT_KEY,
+               .tshow = sc_tag_show_bootkey,
+               .tstore = sc_tag_store_bootkey,
+               .kattr = __ATTR(boot_key, RB_SC_RMODE|RB_SC_WMODE, sc_attr_show, sc_attr_store),
+       }, {
+               .tag_id = RB_SCID_CPU_MODE,
+               .tshow = sc_tag_show_cpumode,
+               .tstore = sc_tag_store_cpumode,
+               .kattr = __ATTR(cpu_mode, RB_SC_RMODE|RB_SC_WMODE, sc_attr_show, sc_attr_store),
+       }, {
+               .tag_id = RB_SCID_BIOS_VERSION,
+               .tshow = routerboot_tag_show_string,
+               .tstore = NULL,
+               .kattr = __ATTR(bios_version, RB_SC_RMODE, sc_attr_show, NULL),
+       }, {
+               .tag_id = RB_SCID_BOOT_PROTOCOL,
+               .tshow = sc_tag_show_bootproto,
+               .tstore = sc_tag_store_bootproto,
+               .kattr = __ATTR(boot_proto, RB_SC_RMODE|RB_SC_WMODE, sc_attr_show, sc_attr_store),
+       }, {
+               .tag_id = RB_SCID_BOOTER,
+               .tshow = sc_tag_show_booter,
+               .tstore = sc_tag_store_booter,
+               .kattr = __ATTR(booter, RB_SC_RMODE|RB_SC_WMODE, sc_attr_show, sc_attr_store),
+       }, {
+               .tag_id = RB_SCID_SILENT_BOOT,
+               .tshow = sc_tag_show_silent_boot,
+               .tstore = sc_tag_store_silent_boot,
+               .kattr = __ATTR(silent_boot, RB_SC_RMODE|RB_SC_WMODE, sc_attr_show, sc_attr_store),
+       },
+       // TODO CPU_FREQ
+};
+
+static ssize_t sc_attr_show(struct kobject *kobj, struct kobj_attribute *attr,
+                           char *buf)
+{
+       const struct sc_attr *sc_attr;
+       const u8 *pld;
+       u16 pld_len;
+
+       sc_attr = container_of(attr, typeof(*sc_attr), kattr);
+
+       if (!sc_attr->pld_len)
+               return -ENOENT;
+
+       pld = sc_buf + sc_attr->pld_ofs;        // pld aliases sc_buf -> lock!
+       pld_len = sc_attr->pld_len;
+
+       return sc_attr->tshow(pld, pld_len, buf);
+}
+
+static ssize_t sc_attr_store(struct kobject *kobj, struct kobj_attribute *attr,
+                            const char *buf, size_t count)
+{
+       const struct sc_attr *sc_attr;
+       const u8 *pld;
+       u16 pld_len;
+
+       if (!RB_SC_HAS_WRITE_SUPPORT)
+               return -EOPNOTSUPP;
+
+       if (!capable(CAP_SYS_ADMIN))
+               return -EACCES;
+
+       sc_attr = container_of(attr, typeof(*sc_attr), kattr);
+
+       if (!sc_attr->tstore)
+               return -EOPNOTSUPP;
+
+       if (!sc_attr->pld_len)
+               return -ENOENT;
+
+       pld = sc_buf + sc_attr->pld_ofs;        // pld aliases sc_buf -> lock!
+       pld_len = sc_attr->pld_len;
+
+       return sc_attr->tstore(pld, pld_len, buf, count);
+}
+
+/*
+ * Shows the current buffer status:
+ * "clean": the buffer is in sync with the mtd data
+ * "dirty": the buffer is out of sync with the mtd data
+ */
+static ssize_t sc_commit_show(struct kobject *kobj, struct kobj_attribute *attr,
+                             char *buf)
+{
+       const char *str;
+       char *out = buf;
+       u32 crc;
+
+       read_lock(&sc_bufrwl);
+       crc = RB_SC_GETCRC();
+       read_unlock(&sc_bufrwl);
+
+       str = (crc) ? "clean" : "dirty";
+       out += sprintf(out, "%s\n", str);
+
+       return out - buf;
+}
+
+/*
+ * Performs buffer flushing:
+ * This routine expects an input compatible with kstrtobool().
+ * - a "false" input discards the current changes and reads data back from mtd.
+ * - a "true" input commits the current changes to mtd.
+ * If there is no pending changes, this routine is a no-op.
+ * Handling failures is left as an exercise to userspace.
+ */
+static ssize_t sc_commit_store(struct kobject *kobj, struct kobj_attribute *attr,
+                             const char *buf, size_t count)
+{
+       struct mtd_info *mtd;
+       struct erase_info ei;
+       size_t bytes_rw, ret = count;
+       bool flush;
+       u32 crc;
+
+       if (!RB_SC_HAS_WRITE_SUPPORT)
+               return -EOPNOTSUPP;
+
+       read_lock(&sc_bufrwl);
+       crc = RB_SC_GETCRC();
+       read_unlock(&sc_bufrwl);
+
+       if (crc)
+               return count;   // NO-OP
+
+       ret = kstrtobool(buf, &flush);
+       if (ret)
+               return ret;
+
+       mtd = get_mtd_device_nm(RB_MTD_SOFT_CONFIG);    // TODO allow override
+       if (IS_ERR(mtd))
+               return -ENODEV;
+
+       write_lock(&sc_bufrwl);
+       if (!flush)     // reread
+               ret = mtd_read(mtd, 0, mtd->size, &bytes_rw, sc_buf);
+       else {  // crc32 + commit
+               /*
+                * CRC32 is computed on the entire buffer, excluding the CRC
+                * value itself. CRC is already null when we reach this point,
+                * so we can compute the CRC32 on the buffer as is.
+                * The expected CRC32 is Ethernet FCS style, meaning the seed is
+                * ~0 and the final result is also bitflipped.
+                */
+
+               crc = ~crc32(~0, sc_buf, sc_buflen);
+               RB_SC_SETCRC(crc);
+
+               /*
+                * The soft_config partition is assumed to be entirely contained
+                * in a single eraseblock.
+                */
+
+               ei.addr = 0;
+               ei.len = mtd->size;
+               ret = mtd_erase(mtd, &ei);
+               if (!ret)
+                       ret = mtd_write(mtd, 0, mtd->size, &bytes_rw, sc_buf);
+
+               /*
+                * Handling mtd_write() failure here is a tricky situation. The
+                * proposed approach is to let userspace deal with retrying,
+                * with the caveat that it must try to flush the buffer again as
+                * rereading the mtd contents could potentially read garbage.
+                * The rationale is: even if we keep a shadow buffer of the
+                * original content, there is no guarantee that we will ever be
+                * able to write it anyway.
+                * Regardless, it appears that RouterBOOT will ignore an invalid
+                * soft_config (including a completely wiped segment) and will
+                * write back factory defaults when it happens.
+                */
+       }
+       write_unlock(&sc_bufrwl);
+
+       if (ret)
+               goto mtdfail;
+
+       if (bytes_rw != sc_buflen) {
+               ret = -EIO;
+               goto mtdfail;
+       }
+
+       return count;
+
+mtdfail:
+       RB_SC_CLRCRC(); // mark buffer content as dirty/invalid
+       return ret;
+}
+
+static struct kobj_attribute sc_kattrcommit = __ATTR(commit, RB_SC_RMODE|RB_SC_WMODE, sc_commit_show, sc_commit_store);
+
+int __init rb_softconfig_init(struct kobject *rb_kobj)
+{
+       struct mtd_info *mtd;
+       size_t bytes_read, buflen;
+       const u8 *buf;
+       int i, ret;
+       u32 magic;
+
+       sc_buf = NULL;
+       sc_kobj = NULL;
+
+       // TODO allow override
+       mtd = get_mtd_device_nm(RB_MTD_SOFT_CONFIG);
+       if (IS_ERR(mtd))
+               return -ENODEV;
+
+       sc_buflen = mtd->size;
+       sc_buf = kmalloc(sc_buflen, GFP_KERNEL);
+       if (!sc_buf)
+               return -ENOMEM;
+
+       ret = mtd_read(mtd, 0, sc_buflen, &bytes_read, sc_buf);
+
+       if (ret)
+               goto fail;
+
+       if (bytes_read != sc_buflen) {
+               ret = -EIO;
+               goto fail;
+       }
+
+       /* Check we have what we expect */
+       magic = *(const u32 *)sc_buf;
+       if (RB_MAGIC_SOFT != magic) {
+               ret = -EINVAL;
+               goto fail;
+       }
+
+       /* Skip magic and 32bit CRC located immediately after */
+       buf = sc_buf + (sizeof(magic) + sizeof(u32));
+       buflen = sc_buflen - (sizeof(magic) + sizeof(u32));
+
+       /* Populate sysfs */
+       ret = -ENOMEM;
+       sc_kobj = kobject_create_and_add(RB_MTD_SOFT_CONFIG, rb_kobj);
+       if (!sc_kobj)
+               goto fail;
+
+       rwlock_init(&sc_bufrwl);
+
+       /* Locate and publish all known tags */
+       for (i = 0; i < ARRAY_SIZE(sc_attrs); i++) {
+               ret = routerboot_tag_find(buf, buflen, sc_attrs[i].tag_id,
+                                         &sc_attrs[i].pld_ofs, &sc_attrs[i].pld_len);
+               if (ret) {
+                       sc_attrs[i].pld_ofs = sc_attrs[i].pld_len = 0;
+                       continue;
+               }
+
+               /* Account for skipped magic and crc32 */
+               sc_attrs[i].pld_ofs += sizeof(magic) + sizeof(u32);
+
+               ret = sysfs_create_file(sc_kobj, &sc_attrs[i].kattr.attr);
+               if (ret)
+                       pr_warn(RB_SC_PR_PFX "Could not create %s sysfs entry (%d)\n",
+                              sc_attrs[i].kattr.attr.name, ret);
+       }
+
+       /* Finally add the 'commit' attribute */
+       if (RB_SC_HAS_WRITE_SUPPORT) {
+               ret = sysfs_create_file(sc_kobj, &sc_kattrcommit.attr);
+               if (ret) {
+                       pr_err(RB_SC_PR_PFX "Could not create %s sysfs entry (%d), aborting!\n",
+                              sc_kattrcommit.attr.name, ret);
+                       goto sysfsfail; // required attribute
+               }
+       }
+
+       pr_info("MikroTik RouterBOARD software configuration sysfs driver v" RB_SOFTCONFIG_VER "\n");
+
+       return 0;
+
+sysfsfail:
+       kobject_put(sc_kobj);
+       sc_kobj = NULL;
+fail:
+       kfree(sc_buf);
+       sc_buf = NULL;
+       return ret;
+}
+
+void __exit rb_softconfig_exit(void)
+{
+       kobject_put(sc_kobj);
+       kfree(sc_buf);
+}
index 96a100a933c9f0d29d1640023fc2c79f4bc95318..f496dd7e0c766b2ae08e3daaf687c836742eb174 100644 (file)
@@ -166,11 +166,20 @@ static int __init routerboot_init(void)
        if (!rb_kobj)
                return -ENOMEM;
 
-       return rb_hardconfig_init(rb_kobj);
+       /*
+        * We ignore the following return values and always register.
+        * These init() routines are designed so that their failed state is
+        * always manageable by the corresponding exit() calls.
+        */
+       rb_hardconfig_init(rb_kobj);
+       rb_softconfig_init(rb_kobj);
+
+       return 0;
 }
 
 static void __exit routerboot_exit(void)
 {
+       rb_softconfig_exit();
        rb_hardconfig_exit();
        kobject_put(rb_kobj);   // recursive afaict
 }
index e1ae972183e8a6a65555ad7e54010258ff32caa7..5b644db4fef89bc939b678016b95f11bd03477ef 100644 (file)
@@ -28,6 +28,9 @@ int routerboot_rle_decode(const u8 *in, size_t inlen, u8 *out, size_t *outlen);
 int __init rb_hardconfig_init(struct kobject *rb_kobj);
 void __exit rb_hardconfig_exit(void);
 
+int __init rb_softconfig_init(struct kobject *rb_kobj);
+void __exit rb_softconfig_exit(void);
+
 ssize_t routerboot_tag_show_string(const u8 *pld, u16 pld_len, char *buf);
 
 #endif /* _ROUTERBOOT_H_ */