/* Copyright 2013-2017 IBM Corp. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gard.h" #define FDT_PATH "/proc/device-tree" #define FDT_FSP_NODE FDT_PATH"/fsps" #define FDT_ACTIVE_FLASH_PATH FDT_PATH"/chosen/ibm,system-flash" #define SYSFS_MTD_PATH "/sys/class/mtd/" #define FLASH_GARD_PART "GUARD" #define VPNOR_GARD_DIR "/media/pnor-prsv" #define VPNOR_GARD_FILE VPNOR_GARD_DIR"/GUARD" /* Full gard version number (possibly includes gitid). */ extern const char version[]; #define __unused __attribute__((unused)) struct gard_ctx { uint32_t f_size; uint32_t f_pos; uint32_t gard_part_idx; uint32_t gard_data_pos; uint32_t gard_data_len; struct blocklevel_device *bl; struct ffs_handle *ffs; }; static void show_flash_err(int rc) { switch (rc) { case FFS_ERR_BAD_MAGIC: fprintf(stderr, "libffs bad magic\n"); break; case FFS_ERR_BAD_VERSION: fprintf(stderr, "libffs bad version\n"); break; case FFS_ERR_BAD_CKSUM: fprintf(stderr, "libffs bad check sum\n"); break; case FFS_ERR_PART_NOT_FOUND: fprintf(stderr, "libffs flash partition not found\n"); break; /* ------- */ case FLASH_ERR_MALLOC_FAILED: fprintf(stderr, "libflash malloc failed\n"); break; case FLASH_ERR_CHIP_UNKNOWN: fprintf(stderr, "libflash unknown flash chip\n"); break; case FLASH_ERR_PARM_ERROR: fprintf(stderr, "libflash parameter error\n"); break; case FLASH_ERR_ERASE_BOUNDARY: fprintf(stderr, "libflash erase boundary error\n"); break; case FLASH_ERR_WREN_TIMEOUT: fprintf(stderr, "libflash WREN timeout\n"); break; case FLASH_ERR_WIP_TIMEOUT: fprintf(stderr, "libflash WIP timeout\n"); break; case FLASH_ERR_VERIFY_FAILURE: fprintf(stderr, "libflash verification failure\n"); break; case FLASH_ERR_4B_NOT_SUPPORTED: fprintf(stderr, "libflash 4byte mode not supported\n"); break; case FLASH_ERR_CTRL_CONFIG_MISMATCH: fprintf(stderr, "libflash control config mismatch\n"); break; case FLASH_ERR_CHIP_ER_NOT_SUPPORTED: fprintf(stderr, "libflash chip not supported\n"); break; case FLASH_ERR_CTRL_CMD_UNSUPPORTED: fprintf(stderr, "libflash unsupported control command\n"); break; case FLASH_ERR_CTRL_TIMEOUT: fprintf(stderr, "libflash control timeout\n"); break; case FLASH_ERR_ECC_INVALID: fprintf(stderr, "libflash ecc invalid\n"); break; default: fprintf(stderr, "A libflash/libffs error has occurred %d\n", rc); } } const struct chip_unit_desc *chip_units; int chip_unit_count; static void set_chip_gen(const struct chip_unit_desc *c) { chip_units = c; chip_unit_count = 0; while (strcmp("LAST_IN_RANGE", c->desc)) { chip_unit_count++; c++; } } #ifdef __powerpc64__ static void guess_chip_gen(void) { /* * Guesstimate what chip generation based on the PVR if we're running * on ppc64. */ uint32_t pvr; /* grab the chip type from the PVR SPR */ asm ("mfspr %0,0x11f" : "=r" (pvr)); switch (pvr >> 16) { case 0x004b: /* murano */ case 0x004c: /* naples */ case 0x004d: /* venice */ set_chip_gen(p8_chip_units); return; case 0x004e: /* nimbus */ set_chip_gen(p9_chip_units); return; default: fprintf(stderr, "Unsupported processor (pvr %#x)! Set the processor generation manually with -8 or -9\n", pvr); exit(1); } } #else static void guess_chip_gen(void) { #ifdef ASSUME_P8 set_chip_gen(p8_chip_units); #else set_chip_gen(p9_chip_units); #endif } #endif static const char *target_type_to_str(int type) { int i; for (i = 0; i < chip_unit_count; i++) if (chip_units[i].type == type) return chip_units[i].desc; return "UNKNOWN"; } static int str_to_target_type(const char *path) { int i, len; for (i = 0; i < chip_unit_count; i++) { len = strlen(chip_units[i].desc); if (!strncasecmp(chip_units[i].desc, path, len)) return chip_units[i].type; /* match! */ } return -1; } static const char *deconfig_reason_str(enum gard_reason reason) { switch (reason) { case GARD_NO_REASON: return "None"; case GARD_MANUAL: return "Manual"; case GARD_UNRECOVERABLE: return "Unrecoverable"; case GARD_FATAL: return "Fatal"; case GARD_PREDICTIVE: return "Predictive"; case GARD_POWER: return "Power"; // What does this even mean? case GARD_HYP: return "Hypervisor"; case GARD_RECONFIG: return "Reconfig"; default: return "Unknown"; } }; static const char *path_type_to_str(enum path_type t) { switch (t) { case PATH_NA: return "not applicable"; case PATH_AFFINITY: return "affinity"; case PATH_PHYSICAL: return "physical"; case PATH_DEVICE: return "device"; case PATH_POWER: return "power"; } return "Unknown"; } /* * NB: buffer is assumped to be MAX_PATH_SIZE */ static char *format_path(struct entity_path *path, char *buffer) { int elements = path->type_size & PATH_ELEMENTS_MASK; int i, offset = 0; for (i = 0; i < elements; i++) { const struct path_element *e = &path->path_elements[i]; offset += sprintf(buffer + offset, "/%s%d", target_type_to_str(e->target_type), e->instance); } return buffer; } /* * parses a Path string into the entity_path structured provided. * * str - In param, String to parse * parsed - Out param, resultant entity_path * * e.g. * * "/Sys0/Node0/Proc1" -> { * type_size = 0x23, * * path_element[0] = {0, 0} * path_element[1] = {1, 0} * path_element[2] = {2, 1} * } */ static int parse_path(const char *str, struct entity_path *parsed) { int unit_count = 0; memset(parsed, 0, sizeof(*parsed)); while (*str != '\0') { int unit_id = str_to_target_type(++str); /* ++ skips the '/' */ long instance; char *end; size_t len; if (unit_count > MAX_PATH_ELEMENTS - 1) { fprintf(stderr, "Path has more than 10 components!\n"); return -1; } /* find the type Id of this component */ if (unit_id < 0) { /* unknown unit, bail out */ fprintf(stderr, "Unknown unit at: '%s'\n", str); return -1; } parsed->path_elements[unit_count].target_type = unit_id; /* now parse the instance # */ len = strlen(target_type_to_str(unit_id)); instance = strtol(str + len, &end, 10); if (!isdigit(*(str + len))) { fprintf(stderr, "Missing instance number after '%s'\n", str); return -1; } if (*end != '\0' && *end != '/') { fprintf(stderr, "Unable to parse instance after '%s'\n", str); return -1; } if (instance > 255 || instance < 0) { fprintf(stderr, "Instance %ld is invalid. Must be 0 to 255\n", instance); return -1; } parsed->path_elements[unit_count].instance = instance; str = end; unit_count++; } /* * We assume the path is a physical path because every gard record I've * seen so far uses them. We might need to fix this later on, but lets * cross the bridge when we have to. */ parsed->type_size = (unit_count & 0xf) | (PATH_PHYSICAL << PATH_TYPE_SHIFT); return 0; } static struct gard_record blank_record; static bool is_valid_record(struct gard_record *g) { return memcmp(&blank_record, g, sizeof(*g)); } static int do_iterate(struct gard_ctx *ctx, int (*func)(struct gard_ctx *ctx, int pos, struct gard_record *gard, void *priv), void *priv) { int rc = 0; unsigned int i; struct gard_record gard, null_gard; memset(&null_gard, UINT_MAX, sizeof(gard)); for (i = 0; i * sizeof(gard) < ctx->gard_data_len && rc == 0; i++) { memset(&gard, 0, sizeof(gard)); rc = blocklevel_read(ctx->bl, ctx->gard_data_pos + (i * sizeof(gard)), &gard, sizeof(gard)); /* It isn't super clear what constitutes the end, this should do */ if (rc || memcmp(&gard, &null_gard, sizeof(gard)) == 0) break; rc = func(ctx, i, &gard, priv); } return rc; } /* * read the next guard record into the supplied buffer (gard) * * returns the record id (nb: 1 based not zero) * */ static int __gard_next(struct gard_ctx *ctx, int pos, struct gard_record *gard, int *rc) { uint32_t offset = pos * sizeof(*gard); if (offset > ctx->gard_data_len) /* too big */ return -1; /* you lose error handling information, *gruble* */ memset(gard, 0, sizeof(*gard)); *rc = blocklevel_read(ctx->bl, ctx->gard_data_pos + offset, gard, sizeof(*gard)); if (!is_valid_record(gard)) return -1; if (*rc) return -1; return pos; } #define for_each_gard(ctx, pos, gard, rc) \ for (pos = __gard_next(ctx, 0, gard, rc); \ pos >= 0; pos = __gard_next(ctx, ++pos, gard, rc)) static int count_records(struct gard_ctx *ctx) { struct gard_record record; int rc, pos, count = 0; for_each_gard(ctx, pos, &record, &rc) count++; return rc ? rc : count; } static int count_valid_records(struct gard_ctx *ctx) { struct gard_record record; int rc, pos, count = 0; for_each_gard(ctx, pos, &record, &rc) count++; return rc ? rc : count; } static size_t find_longest_path(struct gard_ctx *ctx) { char scratch[MAX_PATH_SIZE]; struct gard_record gard; size_t len, longest = 0; int rc, pos; for_each_gard(ctx, pos, &gard, &rc) { len = strlen(format_path(&gard.target_id, scratch)); if (len > longest) longest = len; } return longest; } static void draw_ruler(char c, int size) { int i; for (i = 0; i < size; i++) putchar(c); putchar('\n'); } static int do_list(struct gard_ctx *ctx, int argc __attribute__((unused)), char **argv __attribute__((unused))) { /* This header matches the line formatting above in do_list_i() */ const char *header = " ID | Error | Type | Path"; size_t ruler_size; char scratch[MAX_PATH_SIZE]; struct gard_record gard; int rc = 0, pos; /* No entries */ if (count_valid_records(ctx) == 0) { printf("No GARD entries to display\n"); return 0; } puts(header); ruler_size = strlen(header) + find_longest_path(ctx); draw_ruler('-', ruler_size); for_each_gard(ctx, pos, &gard, &rc) { printf(" %08x | %08x | %-10s | %s%s\n", be32toh(gard.record_id), be32toh(gard.errlog_eid), deconfig_reason_str(gard.error_type), format_path(&gard.target_id, scratch), gard.record_id == 0xffffffff ? " *CLEARED*" : ""); } draw_ruler('=', ruler_size); return rc; } static int do_show_i(struct gard_ctx *ctx, int pos, struct gard_record *gard, void *priv) { uint32_t id; (void)ctx; (void)pos; if (!priv || !gard) return -1; id = *(uint32_t *)priv; if (be32toh(gard->record_id) == id) { unsigned int count, i; printf("Record ID: 0x%08x%s\n", id, id == 0xffffffff ? " *CLEARED*" : ""); printf("========================\n"); printf("Error ID: 0x%08x\n", be32toh(gard->errlog_eid)); printf("Error Type: %s (0x%02x)\n", deconfig_reason_str(gard->error_type), gard->error_type); printf("Path Type: %s\n", path_type_to_str(gard->target_id.type_size >> PATH_TYPE_SHIFT)); count = gard->target_id.type_size & PATH_ELEMENTS_MASK; for (i = 0; i < count && i < MAX_PATH_ELEMENTS; i++) printf("%*c%s, Instance #%d\n", i + 1, '>', target_type_to_str(gard->target_id.path_elements[i].target_type), gard->target_id.path_elements[i].instance); } return 0; } static int do_show(struct gard_ctx *ctx, int argc, char **argv) { uint32_t id; int rc; if (argc != 2) { fprintf(stderr, "%s option requires a GARD record\n", argv[0]); return -1; } id = strtoul(argv[1], NULL, 16); rc = do_iterate(ctx, &do_show_i, &id); return rc; } static int do_clear_i(struct gard_ctx *ctx, int pos, struct gard_record *gard, void *priv) { int largest, rc = 0; char *buf; struct gard_record null_gard; if (!gard || !ctx || !priv) return -1; /* Not this one */ if (be32toh(gard->record_id) != *(uint32_t *)priv) return 0; memset(&null_gard, 0xFF, sizeof(null_gard)); largest = count_records(ctx); printf("Clearing gard record 0x%08x...", be32toh(gard->record_id)); if (largest < 0 || pos > largest) { /* Something went horribly wrong */ fprintf(stderr, "largest index out of range %d\n", largest); return -1; } if (pos < largest) { /* We're not clearing the last record, shift all the records up */ int buf_len = ((largest - pos) * sizeof(struct gard_record)); int buf_pos = ctx->gard_data_pos + ((pos + 1) * sizeof(struct gard_record)); buf = malloc(buf_len); if (!buf) return -ENOMEM; rc = blocklevel_read(ctx->bl, buf_pos, buf, buf_len); if (rc) { free(buf); fprintf(stderr, "Couldn't read from flash at 0x%08x for len 0x%08x\n", buf_pos, buf_len); return rc; } rc = blocklevel_smart_write(ctx->bl, buf_pos - sizeof(gard), buf, buf_len); free(buf); if (rc) { fprintf(stderr, "Couldn't write to flash at 0x%08x for len 0x%08x\n", buf_pos - (int) sizeof(struct gard_record), buf_len); return rc; } } /* Now wipe the last record */ rc = blocklevel_smart_write(ctx->bl, ctx->gard_data_pos + (largest * sizeof(null_gard)), &null_gard, sizeof(null_gard)); printf("done\n"); return rc; } static int reset_partition(struct gard_ctx *ctx) { int no_ecc_len = (ctx->gard_data_len / 9) * 8; struct gard_record *gard; int rc = 0; gard = malloc(ctx->gard_data_len); if (!gard) { return FLASH_ERR_MALLOC_FAILED; } memset(gard, 0xFF, ctx->gard_data_len); rc = blocklevel_smart_erase(ctx->bl, ctx->gard_data_pos, ctx->gard_data_len); if (rc) { fprintf(stderr, "Couldn't erase the gard partition. Bailing out\n"); goto out; } rc = blocklevel_write(ctx->bl, ctx->gard_data_pos, gard, no_ecc_len); if (rc) fprintf(stderr, "Couldn't reset the entire gard partition. Bailing out\n"); out: free(gard); return rc; } static int do_clear(struct gard_ctx *ctx, int argc, char **argv) { int rc; uint32_t id; if (argc != 2) { fprintf(stderr, "%s option requires a GARD record or 'all'\n", argv[0]); return -1; } if (strncmp(argv[1], "all", strlen("all")) == 0) { printf("Clearing the entire gard partition..."); fflush(stdout); rc = reset_partition(ctx); printf("done\n"); } else { id = strtoul(argv[1], NULL, 16); rc = do_iterate(ctx, do_clear_i, &id); } return rc; } static int do_create(struct gard_ctx *ctx, int argc, char **argv) { int rc, pos, max_id = 0, last_pos = 0; struct gard_record gard; struct entity_path path; if (argc < 2) { fprintf(stderr, "create requires path to gard\n"); fprintf(stderr, "e.g.\n"); fprintf(stderr, " /Sys0/Node0/Proc0\n"); fprintf(stderr, " /Sys0/Node0/DIMM15\n"); return -1; } if (parse_path(argv[1], &path)) { fprintf(stderr, "Unable to parse path\n"); return -1; } /* check if we already have a gard record applied to this path */ for_each_gard(ctx, pos, &gard, &rc) { if (!memcmp(&path, &gard.target_id, sizeof(path))) { fprintf(stderr, "Unit %s is already GARDed by record %#08x\n", argv[1], be32toh(gard.record_id)); return -1; } /* * Keep track of the largest record ID seen so far, * we'll give the new record the max + 1 to ensure * that it's unique */ if (be32toh(gard.record_id) > max_id) max_id = be32toh(gard.record_id); last_pos++; } /* do we have an empty record to write into? */ if (!rc && !is_valid_record(&gard)) { int offset = last_pos * sizeof(gard); memset(&gard, 0xff, sizeof(gard)); gard.record_id = be32toh(max_id + 1); gard.error_type = GARD_MANUAL; gard.target_id = path; gard.errlog_eid = 0x0; if (offset > ctx->gard_data_len - sizeof(gard)) { fprintf(stderr, "No space in GUARD for a new record\n"); return -1; } rc = blocklevel_smart_write(ctx->bl, ctx->gard_data_pos + offset, &gard, sizeof(gard)); } return rc; } static int check_gard_partition(struct gard_ctx *ctx) { int rc; struct gard_record gard; char msg[2]; if (ctx->gard_data_len == 0 || ctx->gard_data_len % sizeof(struct gard_record) != 0) /* Just warn for now */ fprintf(stderr, "The %s partition doesn't appear to be an exact multiple of" "gard records in size: %zd vs %u (or partition is zero in length)\n", FLASH_GARD_PART, sizeof(struct gard_record), ctx->gard_data_len); /* * Attempt to read the first record, nothing can really operate if the * first record is dead. There (currently) isn't a way to validate more * than ECC correctness. */ rc = blocklevel_read(ctx->bl, ctx->gard_data_pos, &gard, sizeof(gard)); if (rc == FLASH_ERR_ECC_INVALID) { fprintf(stderr, "The data at the GUARD partition does not appear to be valid gard data\n"); fprintf(stderr, "Clear the entire GUARD partition? [y/N]\n"); if (fgets(msg, sizeof(msg), stdin) == NULL) { fprintf(stderr, "Couldn't read from standard input\n"); return -1; } if (msg[0] == 'y') { rc = reset_partition(ctx); if (rc) { fprintf(stderr, "Couldn't reset the GUARD partition. Bailing out\n"); return rc; } } /* * else leave rc as is so that the main bails out, not going to be * able to do sensible anyway */ } return rc; } __attribute__ ((unused)) static int do_nop(struct gard_ctx *ctx, int argc, char **argv) { (void)ctx; (void)argc; fprintf(stderr, "Unimplemented action '%s'\n", argv[0]); return EXIT_SUCCESS; } struct { const char *name; const char *desc; int (*fn)(struct gard_ctx *, int, char **); } actions[] = { { "list", "List current GARD records", do_list }, { "show", "Show details of a GARD record", do_show }, { "clear", "Clear GARD records", do_clear }, { "create", "Create a GARD record", do_create }, }; static void print_version(void) { printf("Open-Power GARD tool %s\n", version); } static void usage(const char *progname) { unsigned int i; print_version(); fprintf(stderr, "Usage: %s [-a -e -f -p] []\n\n", progname); fprintf(stderr, "-8 --p8\n"); fprintf(stderr, "-9 --p9\n\tSet the processor generation\n\n"); fprintf(stderr, "-e --ecc\n\tForce reading/writing with ECC bytes.\n\n"); fprintf(stderr, "-f --file \n\tDon't search for MTD device," " read from .\n\n"); fprintf(stderr, "-p --part\n\tUsed in conjunction with -f to specify" " that just\n"); fprintf(stderr, "\tthe GUARD partition is in and libffs\n"); fprintf(stderr, "\tshouldn't be used.\n\n"); fprintf(stderr, "Where is one of:\n\n"); for (i = 0; i < ARRAY_SIZE(actions); i++) { fprintf(stderr, "\t%-7s\t%s\n", actions[i].name, actions[i].desc); } } static bool is_fsp(void) { return access(FDT_FSP_NODE, F_OK) == 0; } static struct option global_options[] = { { "file", required_argument, 0, 'f' }, { "part", no_argument, 0, 'p' }, { "ecc", no_argument, 0, 'e' }, { "p8", no_argument, 0, '8' }, { "p9", no_argument, 0, '9' }, { 0 }, }; static const char *global_optstring = "+ef:p89"; int main(int argc, char **argv) { const char *action, *progname; char *filename = NULL; struct gard_ctx _ctx, *ctx; uint64_t bl_size; int rc, i = 0; bool part = 0; bool ecc = 0; progname = argv[0]; ctx = &_ctx; memset(ctx, 0, sizeof(*ctx)); memset(&blank_record, 0xff, sizeof(blank_record)); if (is_fsp()) { fprintf(stderr, "This is the OpenPower gard tool which does " "not support FSP systems\n"); return EXIT_FAILURE; } /* process global options */ for (;;) { int c; c = getopt_long(argc, argv, global_optstring, global_options, NULL); if (c == -1) break; switch (c) { case 'e': ecc = true; break; case 'f': /* If they specify -f twice */ free(filename); filename = strdup(optarg); if (!filename) { fprintf(stderr, "Out of memory\n"); return EXIT_FAILURE; } break; case 'p': part = true; break; case '8': set_chip_gen(p8_chip_units); break; case '9': set_chip_gen(p9_chip_units); break; case '?': usage(progname); rc = EXIT_FAILURE; goto out_free; } } /* * It doesn't make sense to specify that we have the gard partition but * read from flash */ if (part && !filename) { usage(progname); fprintf(stderr, "-p only makes sense when used with -f!\n"); return EXIT_FAILURE; } /* do we have a command? */ if (optind == argc) { usage(progname); rc = EXIT_FAILURE; goto out_free; } argc -= optind; argv += optind; action = argv[0]; #ifdef __arm__ /* * HACK: Look for a vPNOR GUARD file if we haven't been given anything * explitly. If it exists then we can safely assume that: * a) The host is a P9 * b) The file is ECC protected * c) The file is a bare partition. * * This is a stupid hack, but there's not other sane place for it. * arch_init_flash() always looks for a FFS formatted PNOR when * filename is NULL */ if (!filename) { struct stat buf; if (!stat(VPNOR_GARD_FILE, &buf)) { filename = strdup(VPNOR_GARD_FILE); /* BUG: This ignores the command line settings */ part = true; ecc = true; } else if (!stat(VPNOR_GARD_DIR, &buf)) { printf(VPNOR_GARD_FILE" is missing. Nothing to do\n"); return 0; } } #endif if (!chip_units) guess_chip_gen(); /* * Force libflash to do flash accesses via the MTD. Direct mode is * generally unsafe since it fiddles with the flash controller state * underneath the kernel. Anyone who needs direct mode can use pflash * instead. */ arch_flash_access(ctx->bl, PNOR_MTD); if (arch_flash_init(&(ctx->bl), filename, true)) { /* Can fail for a few ways, most likely couldn't open MTD device */ fprintf(stderr, "Can't open %s\n", filename ? filename : "MTD Device. Are you root?"); rc = EXIT_FAILURE; goto out_free; } rc = blocklevel_get_info(ctx->bl, NULL, &bl_size, NULL); if (rc) goto out; if (bl_size > UINT_MAX) { fprintf(stderr, "MTD device bigger than %i: size: %" PRIu64 "\n", UINT_MAX, bl_size); rc = EXIT_FAILURE; goto out; } ctx->f_size = bl_size; if (!part) { rc = ffs_init(0, ctx->f_size, ctx->bl, &ctx->ffs, 1); if (rc) goto out; rc = ffs_lookup_part(ctx->ffs, FLASH_GARD_PART, &ctx->gard_part_idx); if (rc) goto out; rc = ffs_part_info(ctx->ffs, ctx->gard_part_idx, NULL, &(ctx->gard_data_pos), &(ctx->gard_data_len), NULL, NULL); if (rc) goto out; } else { if (ecc) { rc = blocklevel_ecc_protect(ctx->bl, 0, ctx->f_size); if (rc) goto out; } ctx->gard_data_pos = 0; ctx->gard_data_len = ctx->f_size; } rc = check_gard_partition(ctx); if (rc) { fprintf(stderr, "Does not appear to be sane gard data\n"); goto out; } for (i = 0; i < ARRAY_SIZE(actions); i++) { if (!strcmp(actions[i].name, action)) { rc = actions[i].fn(ctx, argc, argv); break; } } out: if (ctx->ffs) ffs_close(ctx->ffs); file_exit_close(ctx->bl); if (i == ARRAY_SIZE(actions)) { fprintf(stderr, "%s: '%s' isn't a valid command\n", progname, action); usage(progname); rc = EXIT_FAILURE; goto out_free; } if (rc > 0) { show_flash_err(rc); if (filename && rc == FFS_ERR_BAD_MAGIC) fprintf(stderr, "Maybe you didn't give a full flash image file?\nDid you mean '--part'?\n"); } out_free: free(filename); return rc; }