mmc-utils: Add blockprotect command set

This patch adds a group of commands that allow controlling an eMMC's
user partition write-protect features, which allow temporary, permanent
or until-next-power-on write-protection of a write-protect block (which
is a device-dependent multiple of an erase block).

BUG=None
TEST=Manual enabling, disabling and writing of a bunch of blocks on my
Minnie.

Change-Id: I75ff069f7f9cf94cdb3adfa0a5f4574f912cad9f
Signed-off-by: Julius Werner <jwerner@chromium.org>
Reviewed-on: https://chromium-review.googlesource.com/340252
Reviewed-by: Gwendal Grignou <gwendal@google.com>
diff --git a/mmc.c b/mmc.c
index fa872d5..019f5c3 100644
--- a/mmc.c
+++ b/mmc.c
@@ -204,6 +204,31 @@
 		  "The device path should specify the scr file directory.",
 	  NULL
 	},
+	{ do_blockprotect_enable, -2, "blockprotect enable",
+		"[-p|-r] <device> <sector>\n"
+		"Enable block protection for a given sector. Will write protect all\n"
+		"sectors within the target sector's write protect block. Write protect\n"
+		"block size is a multiple of erase block size and device dependent.\n"
+		"Run mmc blockprotect info to query write protect block size.\n"
+		"  -p  Protect block permanently. WARNING: THIS IS IRREVERSIBLE!\n"
+		"  -r  Protect block until next power-on.",
+	  NULL
+	},
+	{ do_blockprotect_disable, -2, "blockprotect disable",
+		"<device> <sector>\n"
+		"Disable block protection for a given sector. Will disable protection\n"
+		"for all sectors within the target sector's write protect block.\n"
+		"Cannot disable permanent or power-on write protection.",
+	  NULL
+	},
+	{ do_blockprotect_read, -2, "blockprotect read", "<device> <sector>\n"
+		"Query the current block protection status of a given sector.",
+	  NULL
+	},
+	{ do_blockprotect_info, -1, "blockprotect info", "<device>\n"
+		"Query information about device's block protect capabilities.",
+	  NULL
+	},
 	{ 0, 0, 0, 0 }
 };
 
diff --git a/mmc.h b/mmc.h
index f89cf28..95e7c96 100644
--- a/mmc.h
+++ b/mmc.h
@@ -31,6 +31,9 @@
 #define MMC_SWITCH		6	/* ac	[31:0] See below	R1b */
 #define MMC_SEND_EXT_CSD	8	/* adtc				R1  */
 #define MMC_SEND_STATUS		13	/* ac   [31:16] RCA        R1  */
+#define MMC_SET_WRITE_PROT	28	/* ac	[31:0] block number	R1b */
+#define MMC_CLR_WRITE_PROT	29	/* ac	[31:0] block number	R1b */
+#define MMC_SEND_WRITE_PROT_TYPE 31	/* adtc	[31:0] block number	R1 */
 #define R1_SWITCH_ERROR   (1 << 7)  /* sx, c */
 #define MMC_SWITCH_MODE_WRITE_BYTE	0x03	/* Set target to value */
 #define MMC_READ_MULTIPLE_BLOCK  18   /* adtc [31:0] data addr   R1  */
@@ -58,6 +61,7 @@
 #define EXT_CSD_BOOT_BUS_CONDITIONS	177
 #define EXT_CSD_ERASE_GROUP_DEF		175
 #define EXT_CSD_BOOT_WP			173
+#define EXT_CSD_USER_WP			171
 #define EXT_CSD_WR_REL_SET		167
 #define EXT_CSD_WR_REL_PARAM		166
 #define EXT_CSD_SANITIZE_START		165
@@ -92,6 +96,7 @@
 #define EXT_CSD_NATIVE_SECTOR_SIZE	63 /* R */
 #define EXT_CSD_USE_NATIVE_SECTOR	62 /* R/W */
 #define EXT_CSD_DATA_SECTOR_SIZE	61 /* R */
+#define EXT_CSD_CLASS_6_CTRL		59
 #define EXT_CSD_EXT_PARTITIONS_ATTRIBUTE_1	53
 #define EXT_CSD_EXT_PARTITIONS_ATTRIBUTE_0	52
 #define EXT_CSD_CACHE_CTRL		33
@@ -140,6 +145,10 @@
 #define EXT_CSD_ENH_2			(1<<2)
 #define EXT_CSD_ENH_1			(1<<1)
 #define EXT_CSD_ENH_USR			(1<<0)
+#define EXT_CSD_US_PERM_WP_DIS		(1<<4)
+#define EXT_CSD_US_PWR_WP_DIS		(1<<3)
+#define EXT_CSD_US_PERM_WP_EN		(1<<2)
+#define EXT_CSD_US_PWR_WP_EN		(1<<0)
 #define EXT_CSD_REV_V5_1		8
 #define EXT_CSD_REV_V5_0		7
 #define EXT_CSD_REV_V4_5		6
diff --git a/mmc_cmds.c b/mmc_cmds.c
index 3c4c937..faa8df3 100644
--- a/mmc_cmds.c
+++ b/mmc_cmds.c
@@ -40,6 +40,9 @@
 #define FFU_DATA_SIZE	512
 #define CID_SIZE 16
 
+/* Sending several commands too close together seems to cause timeouts. */
+#define INTER_COMMAND_GAP_US (50 * 1000)
+
 #include "3rdparty/hmac_sha/hmac_sha2.h"
 
 int read_extcsd(int fd, __u8 *ext_csd)
@@ -1621,6 +1624,223 @@
 
 }
 
+enum blockprotect_mode {
+	BLOCKPROTECT_TEMPORARY = 0,
+	BLOCKPROTECT_POWERON,
+	BLOCKPROTECT_PERMANENT,
+};
+
+int write_blockprotect(int fd, __u32 sector, int enable)
+{
+	struct mmc_ioc_cmd cmd;
+	int ret;
+
+	memset(&cmd, 0, sizeof(cmd));
+	cmd.write_flag = 1;
+	cmd.opcode = enable ? MMC_SET_WRITE_PROT : MMC_CLR_WRITE_PROT;
+	cmd.arg = sector;
+	cmd.flags = MMC_RSP_SPI_R1B | MMC_RSP_R1B | MMC_CMD_AC;
+
+	ret = ioctl(fd, MMC_IOC_CMD, &cmd);
+	if (ret)
+		perror("SET/CLR_WRITE_PROT command");
+	return ret;
+}
+
+int do_blockprotect_enable(int nargs, char **argv)
+{
+	__u8 ext_csd[EXT_CSD_SIZE];
+	__u8 user_wp;
+	__u32 sector;
+	char *end;
+	int ret, fd;
+	int arg_index = 0;
+	enum blockprotect_mode mode = BLOCKPROTECT_TEMPORARY;
+
+	if (nargs > 0 && !strcmp(argv[1], "-r")) {
+		arg_index++;
+		mode = BLOCKPROTECT_POWERON;
+	} else if (nargs > 0 && !strcmp(argv[1], "-p")) {
+		arg_index++;
+		mode = BLOCKPROTECT_PERMANENT;
+	}
+
+	CHECK(nargs != 3 + arg_index, "Usage: mmc blockprotect enable [-p|-r] <device> <write protect block>\n", exit(1));
+
+	sector = strtoul(argv[2 + arg_index], &end, 0);
+	if (*end != '\0') {
+		fprintf(stderr, "Not a block number: %s\n",
+			argv[2 + arg_index]);
+		exit(1);
+	}
+
+	fd = open(argv[1 + arg_index], O_RDWR);
+	if (fd < 0) {
+		perror("open");
+		exit(1);
+	}
+
+	if (read_extcsd(fd, ext_csd))
+		exit(1);
+
+	user_wp = ext_csd[EXT_CSD_USER_WP];
+	user_wp &= ~(EXT_CSD_US_PERM_WP_EN | EXT_CSD_US_PWR_WP_EN);
+	if (mode == BLOCKPROTECT_POWERON)
+		user_wp |= EXT_CSD_US_PWR_WP_EN;
+	else if (mode == BLOCKPROTECT_PERMANENT)
+		user_wp |= EXT_CSD_US_PERM_WP_EN;
+
+	ret = write_extcsd_value(fd, EXT_CSD_USER_WP, user_wp);
+	if (ret) {
+		perror("update EXT_CSD[USER_WP]");
+		exit(1);
+	}
+
+	usleep(INTER_COMMAND_GAP_US);
+
+	ret = write_blockprotect(fd, sector, 1);
+
+	usleep(INTER_COMMAND_GAP_US);
+
+	user_wp &= ~(EXT_CSD_US_PERM_WP_EN | EXT_CSD_US_PWR_WP_EN);
+	if (write_extcsd_value(fd, EXT_CSD_USER_WP, user_wp)) {
+		perror("reset EXT_CSD[USER_WP]");
+		if (!ret)
+			ret = -1;
+	}
+
+	return ret;
+}
+
+int do_blockprotect_disable(int nargs, char **argv)
+{
+	__u32 sector;
+	char *end;
+	int fd;
+
+	CHECK(nargs != 3, "Usage: mmc blockprotect disable <device> <write protect block>\n", exit(1));
+
+	sector = strtoul(argv[2], &end, 0);
+	if (*end != '\0') {
+		fprintf(stderr, "Not a block number: %s\n", argv[2]);
+		exit(1);
+	}
+
+
+	fd = open(argv[1], O_RDWR);
+	if (fd < 0) {
+		perror("open");
+		exit(1);
+	}
+
+	return write_blockprotect(fd, sector, 0);
+}
+
+int do_blockprotect_read(int nargs, char **argv)
+{
+	__u8 wp_bits[8];
+	__u32 sector;
+	char *end;
+	int fd;
+	struct mmc_ioc_cmd cmd;
+
+	CHECK(nargs != 3, "Usage: mmc blockprotect read <device> <write protect block>\n", exit(1));
+
+	fd = open(argv[1], O_RDWR);
+	if (fd < 0) {
+		perror("open");
+		exit(1);
+	}
+
+	sector = strtoul(argv[2], &end, 0);
+	if (*end != '\0') {
+		fprintf(stderr, "Not a block number: %s\n", argv[2]);
+		exit(1);
+	}
+
+	memset(&cmd, 0, sizeof(cmd));
+	cmd.write_flag = 0;
+	cmd.opcode = MMC_SEND_WRITE_PROT_TYPE;
+	cmd.arg = sector;
+	cmd.flags = MMC_RSP_SPI_R1 | MMC_RSP_R1 | MMC_CMD_ADTC;
+	cmd.blksz = sizeof(wp_bits);
+	cmd.blocks = 1;
+	mmc_ioc_cmd_set_data(cmd, wp_bits);
+
+	if (ioctl(fd, MMC_IOC_CMD, &cmd)) {
+		perror("SEND_WRITE_PROT_TYPE command");
+		exit(1);
+	}
+
+	printf("Sector %u write protection: ", sector);
+	switch (wp_bits[7] & 3) {
+	case 0:
+		printf("NONE\n");
+		break;
+	case 1:
+		printf("TEMPORARY\n");
+		break;
+	case 2:
+		printf("POWER-ON\n");
+		break;
+	case 3:
+		printf("PERMANENT\n");
+		break;
+	}
+
+	return 0;
+}
+
+int do_blockprotect_info(int nargs, char **argv)
+{
+	__u8 ext_csd[EXT_CSD_SIZE];
+	__u8 user_wp;
+	int fd, wp_sz, erase_sz;
+
+	CHECK(nargs != 2, "Usage: mmc blockprotect info <device>\n", exit(1));
+
+	fd = open(argv[1], O_RDWR);
+	if (fd < 0) {
+		perror("open");
+		exit(1);
+	}
+
+	if (read_extcsd(fd, ext_csd))
+		exit(1);
+
+	if (ext_csd[EXT_CSD_CLASS_6_CTRL] != 0) {
+		fprintf(stderr, "Block protection commands not supported: "
+			"CLASS_6_CTRL set.\n");
+		exit(1);
+	}
+
+	if ((ext_csd[EXT_CSD_ERASE_GROUP_DEF] & 0x1) != 0x1) {
+		fprintf(stderr, "Block protection commands not supported: "
+			"high-capacity sizes not enabled.\n");
+		exit(1);
+	}
+
+	wp_sz = get_hc_wp_grp_size(ext_csd);
+	erase_sz = get_hc_erase_grp_size(ext_csd);
+
+	if (erase_sz == 0 || wp_sz == 0) {
+		fprintf(stderr, "Block protection commands not supported: "
+			"no high-capacity size for erase or WP blocks.\n");
+		exit(1);
+	}
+
+	printf("Write protect block size in sectors: %d\n",
+	       erase_sz * wp_sz * 1024);
+
+	user_wp = ext_csd[EXT_CSD_USER_WP];
+	printf("Permanent write protection: %s\n",
+	       user_wp & EXT_CSD_US_PERM_WP_DIS ? "forbidden" : "allowed");
+	printf("Power-on write protection: %s\n",
+	       user_wp & EXT_CSD_US_PWR_WP_DIS ? "forbidden" : "allowed");
+
+	return 0;
+}
+
 static const char* const mmc_ffu_hack_names[] = {
 	[MMC_OVERRIDE_FFU_ARG] = "ffu_arg",
 };
diff --git a/mmc_cmds.h b/mmc_cmds.h
index ff60383..959f370 100644
--- a/mmc_cmds.h
+++ b/mmc_cmds.h
@@ -41,3 +41,7 @@
 int do_read_scr(int argc, char **argv);
 int do_read_cid(int argc, char **argv);
 int do_read_csd(int argc, char **argv);
+int do_blockprotect_enable(int nargs, char **argv);
+int do_blockprotect_disable(int nargs, char **argv);
+int do_blockprotect_read(int nargs, char **argv);
+int do_blockprotect_info(int nargs, char **argv);