merge in nyc-release history after reset to master
diff --git a/Android.mk b/Android.mk
index 22e7802..602a856 100644
--- a/Android.mk
+++ b/Android.mk
@@ -136,4 +136,5 @@
$(LOCAL_PATH)/edify/Android.mk \
$(LOCAL_PATH)/uncrypt/Android.mk \
$(LOCAL_PATH)/updater/Android.mk \
+ $(LOCAL_PATH)/update_verifier/Android.mk \
$(LOCAL_PATH)/applypatch/Android.mk
diff --git a/update_verifier/Android.mk b/update_verifier/Android.mk
new file mode 100644
index 0000000..7f28bce
--- /dev/null
+++ b/update_verifier/Android.mk
@@ -0,0 +1,24 @@
+# Copyright (C) 2015 The Android Open Source Project
+#
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_CLANG := true
+LOCAL_SRC_FILES := update_verifier.cpp
+LOCAL_MODULE := update_verifier
+LOCAL_SHARED_LIBRARIES := libhardware liblog
+
+include $(BUILD_EXECUTABLE)
diff --git a/update_verifier/update_verifier.cpp b/update_verifier/update_verifier.cpp
new file mode 100644
index 0000000..5e88815
--- /dev/null
+++ b/update_verifier/update_verifier.cpp
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * 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.
+ */
+
+/*
+ * This program verifies the integrity of the partitions after an A/B OTA
+ * update. It gets invoked by init, and will only perform the verification if
+ * it's the first boot post an A/B OTA update.
+ *
+ * It relies on dm-verity to capture any corruption on the partitions being
+ * verified. dm-verity must be in enforcing mode, so that it will reboot the
+ * device on dm-verity failures. When that happens, the bootloader should
+ * mark the slot as unbootable and stops trying. We should never see a device
+ * started in dm-verity logging mode but with isSlotBootable equals to 0.
+ *
+ * The current slot will be marked as having booted successfully if the
+ * verifier reaches the end after the verification.
+ *
+ * TODO: The actual verification part will be added later after we have the
+ * A/B OTA package format in place.
+ */
+
+#include <string.h>
+
+#include <hardware/boot_control.h>
+
+#define LOG_TAG "update_verifier"
+#include <log/log.h>
+
+int main(int argc, char** argv) {
+ for (int i = 1; i < argc; i++) {
+ SLOGI("Started with arg %d: %s\n", i, argv[i]);
+ }
+
+ const hw_module_t* hw_module;
+ if (hw_get_module("bootctrl", &hw_module) != 0) {
+ SLOGE("Error getting bootctrl module.\n");
+ return -1;
+ }
+
+ boot_control_module_t* module = reinterpret_cast<boot_control_module_t*>(
+ const_cast<hw_module_t*>(hw_module));
+ module->init(module);
+
+ unsigned current_slot = module->getCurrentSlot(module);
+ int bootable = module->isSlotBootable(module, current_slot);
+ SLOGI("Booting slot %u: isSlotBootable=%d\n", current_slot, bootable);
+
+ if (bootable == 0) {
+ // The current slot has not booted successfully.
+
+ // TODO: Add the actual verification after we have the A/B OTA package
+ // format in place.
+
+ // TODO: Assert the dm-verity mode. Bootloader should never boot a newly
+ // flashed slot (isSlotBootable == 0) with dm-verity logging mode.
+
+ int ret = module->markBootSuccessful(module);
+ if (ret != 0) {
+ SLOGE("Error marking booted successfully: %s\n", strerror(-ret));
+ return -1;
+ }
+ SLOGI("Marked slot %u as booted successfully.\n", current_slot);
+ }
+
+ SLOGI("Leaving update_verifier.\n");
+ return 0;
+}
diff --git a/updater/blockimg.cpp b/updater/blockimg.cpp
index a9d8cc6..50067d4 100644
--- a/updater/blockimg.cpp
+++ b/updater/blockimg.cpp
@@ -65,16 +65,9 @@
std::vector<size_t> pos; // Actual limit is INT_MAX.
};
-static void parse_range(const char* range_text, RangeSet& rs) {
+static void parse_range(const std::string& range_text, RangeSet& rs) {
- if (range_text == nullptr) {
- fprintf(stderr, "failed to parse range: null range\n");
- exit(1);
- }
-
- std::vector<std::string> pieces = android::base::Split(std::string(range_text), ",");
- long int val;
-
+ std::vector<std::string> pieces = android::base::Split(range_text, ",");
if (pieces.size() < 3) {
goto err;
}
@@ -120,7 +113,7 @@
return;
err:
- fprintf(stderr, "failed to parse range '%s'\n", range_text);
+ fprintf(stderr, "failed to parse range '%s'\n", range_text.c_str());
exit(1);
}
@@ -360,25 +353,49 @@
return 0;
}
+// Parameters for transfer list command functions
+struct CommandParameters {
+ std::vector<std::string> tokens;
+ size_t cpos;
+ const char* cmdname;
+ const char* cmdline;
+ std::string freestash;
+ std::string stashbase;
+ bool canwrite;
+ int createdstash;
+ int fd;
+ bool foundwrites;
+ bool isunresumable;
+ int version;
+ size_t written;
+ NewThreadInfo nti;
+ pthread_t thread;
+ std::vector<uint8_t> buffer;
+ uint8_t* patch_start;
+};
+
// Do a source/target load for move/bsdiff/imgdiff in version 1.
-// 'wordsave' is the save_ptr of a strtok_r()-in-progress. We expect
-// to parse the remainder of the string as:
+// We expect to parse the remainder of the parameter tokens as:
//
// <src_range> <tgt_range>
//
// The source range is loaded into the provided buffer, reallocating
// it to make it larger if necessary.
-static int LoadSrcTgtVersion1(char** wordsave, RangeSet& tgt, size_t& src_blocks,
+static int LoadSrcTgtVersion1(CommandParameters& params, RangeSet& tgt, size_t& src_blocks,
std::vector<uint8_t>& buffer, int fd) {
+
+ if (params.cpos + 1 >= params.tokens.size()) {
+ fprintf(stderr, "invalid parameters\n");
+ return -1;
+ }
+
// <src_range>
- char* word = strtok_r(nullptr, " ", wordsave);
RangeSet src;
- parse_range(word, src);
+ parse_range(params.tokens[params.cpos++], src);
// <tgt_range>
- word = strtok_r(nullptr, " ", wordsave);
- parse_range(word, tgt);
+ parse_range(params.tokens[params.cpos++], tgt);
allocate(src.size * BLOCKSIZE, buffer);
int rc = ReadBlocks(src, buffer, fd);
@@ -694,18 +711,15 @@
return 0; // Using existing directory
}
-static int SaveStash(const std::string& base, char** wordsave, std::vector<uint8_t>& buffer,
- int fd, bool usehash) {
- if (!wordsave) {
- return -1;
- }
+static int SaveStash(CommandParameters& params, const std::string& base,
+ std::vector<uint8_t>& buffer, int fd, bool usehash) {
- char *id_tok = strtok_r(nullptr, " ", wordsave);
- if (id_tok == nullptr) {
- fprintf(stderr, "missing id field in stash command\n");
+ // <stash_id> <src_range>
+ if (params.cpos + 1 >= params.tokens.size()) {
+ fprintf(stderr, "missing id and/or src range fields in stash command\n");
return -1;
}
- std::string id(id_tok);
+ const std::string& id = params.tokens[params.cpos++];
size_t blocks = 0;
if (usehash && LoadStash(base, id, true, &blocks, buffer, false) == 0) {
@@ -715,9 +729,8 @@
return 0;
}
- char* word = strtok_r(nullptr, " ", wordsave);
RangeSet src;
- parse_range(word, src);
+ parse_range(params.tokens[params.cpos++], src);
allocate(src.size * BLOCKSIZE, buffer);
if (ReadBlocks(src, buffer, fd) == -1) {
@@ -738,13 +751,12 @@
return WriteStash(base, id, blocks, buffer, false, nullptr);
}
-static int FreeStash(const std::string& base, const char* id) {
- if (base.empty() || id == nullptr) {
+static int FreeStash(const std::string& base, const std::string& id) {
+ if (base.empty() || id.empty()) {
return -1;
}
- std::string fn = GetStashFileName(base, std::string(id), "");
-
+ std::string fn = GetStashFileName(base, id, "");
DeleteFile(fn, nullptr);
return 0;
@@ -768,8 +780,7 @@
}
// Do a source/target load for move/bsdiff/imgdiff in version 2.
-// 'wordsave' is the save_ptr of a strtok_r()-in-progress. We expect
-// to parse the remainder of the string as one of:
+// We expect to parse the remainder of the parameter tokens as one of:
//
// <tgt_range> <src_block_count> <src_range>
// (loads data from source image only)
@@ -785,25 +796,35 @@
// reallocated if needed to accommodate the source data. *tgt is the
// target RangeSet. Any stashes required are loaded using LoadStash.
-static int LoadSrcTgtVersion2(char** wordsave, RangeSet& tgt, size_t& src_blocks,
+static int LoadSrcTgtVersion2(CommandParameters& params, RangeSet& tgt, size_t& src_blocks,
std::vector<uint8_t>& buffer, int fd, const std::string& stashbase, bool* overlap) {
+
+ // At least it needs to provide three parameters: <tgt_range>,
+ // <src_block_count> and "-"/<src_range>.
+ if (params.cpos + 2 >= params.tokens.size()) {
+ fprintf(stderr, "invalid parameters\n");
+ return -1;
+ }
+
// <tgt_range>
- char* word = strtok_r(nullptr, " ", wordsave);
- parse_range(word, tgt);
+ parse_range(params.tokens[params.cpos++], tgt);
// <src_block_count>
- word = strtok_r(nullptr, " ", wordsave);
- android::base::ParseUint(word, &src_blocks);
+ const std::string& token = params.tokens[params.cpos++];
+ if (!android::base::ParseUint(token.c_str(), &src_blocks)) {
+ fprintf(stderr, "invalid src_block_count \"%s\"\n", token.c_str());
+ return -1;
+ }
allocate(src_blocks * BLOCKSIZE, buffer);
// "-" or <src_range> [<src_loc>]
- word = strtok_r(nullptr, " ", wordsave);
- if (word[0] == '-' && word[1] == '\0') {
+ if (params.tokens[params.cpos] == "-") {
// no source ranges, only stashes
+ params.cpos++;
} else {
RangeSet src;
- parse_range(word, src);
+ parse_range(params.tokens[params.cpos++], src);
int res = ReadBlocks(src, buffer, fd);
if (overlap) {
@@ -814,39 +835,39 @@
return -1;
}
- word = strtok_r(nullptr, " ", wordsave);
- if (word == nullptr) {
+ if (params.cpos >= params.tokens.size()) {
// no stashes, only source range
return 0;
}
RangeSet locs;
- parse_range(word, locs);
+ parse_range(params.tokens[params.cpos++], locs);
MoveRange(buffer, locs, buffer);
}
- // <[stash_id:stash-range]>
- char* colonsave;
- while ((word = strtok_r(nullptr, " ", wordsave)) != nullptr) {
+ // <[stash_id:stash_range]>
+ while (params.cpos < params.tokens.size()) {
// Each word is a an index into the stash table, a colon, and
// then a rangeset describing where in the source block that
// stashed data should go.
- colonsave = nullptr;
- char* colon = strtok_r(word, ":", &colonsave);
+ std::vector<std::string> tokens = android::base::Split(params.tokens[params.cpos++], ":");
+ if (tokens.size() != 2) {
+ fprintf(stderr, "invalid parameter\n");
+ return -1;
+ }
std::vector<uint8_t> stash;
- int res = LoadStash(stashbase, std::string(colon), false, nullptr, stash, true);
+ int res = LoadStash(stashbase, tokens[0], false, nullptr, stash, true);
if (res == -1) {
// These source blocks will fail verification if used later, but we
// will let the caller decide if this is a fatal failure
- fprintf(stderr, "failed to load stash %s\n", colon);
+ fprintf(stderr, "failed to load stash %s\n", tokens[0].c_str());
continue;
}
- colon = strtok_r(nullptr, ":", &colonsave);
RangeSet locs;
- parse_range(colon, locs);
+ parse_range(tokens[1], locs);
MoveRange(buffer, locs, stash);
}
@@ -854,25 +875,6 @@
return 0;
}
-// Parameters for transfer list command functions
-struct CommandParameters {
- char* cmdname;
- char* cpos;
- char* freestash;
- std::string stashbase;
- bool canwrite;
- int createdstash;
- int fd;
- bool foundwrites;
- bool isunresumable;
- int version;
- size_t written;
- NewThreadInfo nti;
- pthread_t thread;
- std::vector<uint8_t> buffer;
- uint8_t* patch_start;
-};
-
// Do a source/target load for move/bsdiff/imgdiff in version 3.
//
// Parameters are the same as for LoadSrcTgtVersion2, except for 'onehash', which
@@ -893,26 +895,26 @@
static int LoadSrcTgtVersion3(CommandParameters& params, RangeSet& tgt, size_t& src_blocks,
bool onehash, bool& overlap) {
- char* srchash = strtok_r(nullptr, " ", ¶ms.cpos);
- if (srchash == nullptr) {
+ if (params.cpos >= params.tokens.size()) {
fprintf(stderr, "missing source hash\n");
return -1;
}
- char* tgthash = nullptr;
+ std::string srchash = params.tokens[params.cpos++];
+ std::string tgthash;
+
if (onehash) {
tgthash = srchash;
} else {
- tgthash = strtok_r(nullptr, " ", ¶ms.cpos);
-
- if (tgthash == nullptr) {
+ if (params.cpos >= params.tokens.size()) {
fprintf(stderr, "missing target hash\n");
return -1;
}
+ tgthash = params.tokens[params.cpos++];
}
- if (LoadSrcTgtVersion2(¶ms.cpos, tgt, src_blocks, params.buffer, params.fd,
- params.stashbase, &overlap) == -1) {
+ if (LoadSrcTgtVersion2(params, tgt, src_blocks, params.buffer, params.fd, params.stashbase,
+ &overlap) == -1) {
return -1;
}
@@ -931,7 +933,8 @@
// If source and target blocks overlap, stash the source blocks so we can
// resume from possible write errors
if (overlap) {
- fprintf(stderr, "stashing %zu overlapping blocks to %s\n", src_blocks, srchash);
+ fprintf(stderr, "stashing %zu overlapping blocks to %s\n", src_blocks,
+ srchash.c_str());
bool stash_exists = false;
if (WriteStash(params.stashbase, srchash, src_blocks, params.buffer, true,
@@ -971,9 +974,9 @@
RangeSet tgt;
if (params.version == 1) {
- status = LoadSrcTgtVersion1(¶ms.cpos, tgt, blocks, params.buffer, params.fd);
+ status = LoadSrcTgtVersion1(params, tgt, blocks, params.buffer, params.fd);
} else if (params.version == 2) {
- status = LoadSrcTgtVersion2(¶ms.cpos, tgt, blocks, params.buffer, params.fd,
+ status = LoadSrcTgtVersion2(params, tgt, blocks, params.buffer, params.fd,
params.stashbase, nullptr);
} else if (params.version >= 3) {
status = LoadSrcTgtVersion3(params, tgt, blocks, true, overlap);
@@ -1003,9 +1006,9 @@
}
- if (params.freestash) {
+ if (!params.freestash.empty()) {
FreeStash(params.stashbase, params.freestash);
- params.freestash = nullptr;
+ params.freestash.clear();
}
params.written += tgt.size;
@@ -1014,28 +1017,33 @@
}
static int PerformCommandStash(CommandParameters& params) {
- return SaveStash(params.stashbase, ¶ms.cpos, params.buffer, params.fd,
+ return SaveStash(params, params.stashbase, params.buffer, params.fd,
(params.version >= 3));
}
static int PerformCommandFree(CommandParameters& params) {
+ // <stash_id>
+ if (params.cpos >= params.tokens.size()) {
+ fprintf(stderr, "missing stash id in free command\n");
+ return -1;
+ }
+
if (params.createdstash || params.canwrite) {
- return FreeStash(params.stashbase, params.cpos);
+ return FreeStash(params.stashbase, params.tokens[params.cpos++]);
}
return 0;
}
static int PerformCommandZero(CommandParameters& params) {
- char* range = strtok_r(nullptr, " ", ¶ms.cpos);
- if (range == nullptr) {
+ if (params.cpos >= params.tokens.size()) {
fprintf(stderr, "missing target blocks for zero\n");
return -1;
}
RangeSet tgt;
- parse_range(range, tgt);
+ parse_range(params.tokens[params.cpos++], tgt);
fprintf(stderr, " zeroing %zu blocks\n", tgt.size);
@@ -1066,14 +1074,14 @@
}
static int PerformCommandNew(CommandParameters& params) {
- char* range = strtok_r(nullptr, " ", ¶ms.cpos);
- if (range == nullptr) {
+ if (params.cpos >= params.tokens.size()) {
+ fprintf(stderr, "missing target blocks for new\n");
return -1;
}
RangeSet tgt;
- parse_range(range, tgt);
+ parse_range(params.tokens[params.cpos++], tgt);
if (params.canwrite) {
fprintf(stderr, " writing %zu blocks of new data\n", tgt.size);
@@ -1105,33 +1113,32 @@
static int PerformCommandDiff(CommandParameters& params) {
- const std::string logparams(params.cpos);
- char* value = strtok_r(nullptr, " ", ¶ms.cpos);
-
- if (value == nullptr) {
- fprintf(stderr, "missing patch offset for %s\n", params.cmdname);
+ // <offset> <length>
+ if (params.cpos + 1 >= params.tokens.size()) {
+ fprintf(stderr, "missing patch offset or length for %s\n", params.cmdname);
return -1;
}
- size_t offset = strtoul(value, nullptr, 0);
-
- value = strtok_r(nullptr, " ", ¶ms.cpos);
-
- if (value == nullptr) {
- fprintf(stderr, "missing patch length for %s\n", params.cmdname);
+ size_t offset;
+ if (!android::base::ParseUint(params.tokens[params.cpos++].c_str(), &offset)) {
+ fprintf(stderr, "invalid patch offset\n");
return -1;
}
- size_t len = strtoul(value, nullptr, 0);
+ size_t len;
+ if (!android::base::ParseUint(params.tokens[params.cpos++].c_str(), &len)) {
+ fprintf(stderr, "invalid patch offset\n");
+ return -1;
+ }
RangeSet tgt;
size_t blocks = 0;
bool overlap = false;
int status = 0;
if (params.version == 1) {
- status = LoadSrcTgtVersion1(¶ms.cpos, tgt, blocks, params.buffer, params.fd);
+ status = LoadSrcTgtVersion1(params, tgt, blocks, params.buffer, params.fd);
} else if (params.version == 2) {
- status = LoadSrcTgtVersion2(¶ms.cpos, tgt, blocks, params.buffer, params.fd,
+ status = LoadSrcTgtVersion2(params, tgt, blocks, params.buffer, params.fd,
params.stashbase, nullptr);
} else if (params.version >= 3) {
status = LoadSrcTgtVersion3(params, tgt, blocks, false, overlap);
@@ -1180,13 +1187,13 @@
}
} else {
fprintf(stderr, "skipping %zu blocks already patched to %zu [%s]\n",
- blocks, tgt.size, logparams.c_str());
+ blocks, tgt.size, params.cmdline);
}
}
- if (params.freestash) {
+ if (!params.freestash.empty()) {
FreeStash(params.stashbase, params.freestash);
- params.freestash = nullptr;
+ params.freestash.clear();
}
params.written += tgt.size;
@@ -1210,15 +1217,13 @@
return -1;
}
- char* range = strtok_r(nullptr, " ", ¶ms.cpos);
-
- if (range == nullptr) {
+ if (params.cpos >= params.tokens.size()) {
fprintf(stderr, "missing target blocks for erase\n");
return -1;
}
RangeSet tgt;
- parse_range(range, tgt);
+ parse_range(params.tokens[params.cpos++], tgt);
if (params.canwrite) {
fprintf(stderr, " erasing %zu blocks\n", tgt.size);
@@ -1278,7 +1283,6 @@
static Value* PerformBlockImageUpdate(const char* name, State* state, int /* argc */, Expr* argv[],
const Command* commands, size_t cmdcount, bool dryrun) {
-
CommandParameters params;
memset(¶ms, 0, sizeof(params));
params.canwrite = !dryrun;
@@ -1368,10 +1372,14 @@
}
}
- // The data in transfer_list_value is not necessarily null-terminated, so we need
- // to copy it to a new buffer and add the null that strtok_r will need.
+ // Copy all the lines in transfer_list_value into std::string for
+ // processing.
const std::string transfer_list(transfer_list_value->data, transfer_list_value->size);
std::vector<std::string> lines = android::base::Split(transfer_list, "\n");
+ if (lines.size() < 2) {
+ ErrorAbort(state, "too few lines in the transfer list [%zd]\n", lines.size());
+ return StringValue(strdup(""));
+ }
// First line in transfer list is the version number
if (!android::base::ParseInt(lines[0].c_str(), ¶ms.version, 1, 4)) {
@@ -1394,6 +1402,11 @@
size_t start = 2;
if (params.version >= 2) {
+ if (lines.size() < 4) {
+ ErrorAbort(state, "too few lines in the transfer list [%zu]\n", lines.size());
+ return StringValue(strdup(""));
+ }
+
// Third line is how many stash entries are needed simultaneously
fprintf(stderr, "maximum stash entries %s\n", lines[2].c_str());
@@ -1405,7 +1418,6 @@
}
int res = CreateStash(state, stash_max_blocks, blockdev_filename->data, params.stashbase);
-
if (res == -1) {
return StringValue(strdup(""));
}
@@ -1433,17 +1445,15 @@
continue;
}
- char* line = strdup(line_str.c_str());
- params.cmdname = strtok_r(line, " ", ¶ms.cpos);
-
- if (params.cmdname == nullptr) {
- fprintf(stderr, "missing command [%s]\n", line);
- goto pbiudone;
- }
+ params.tokens = android::base::Split(line_str, " ");
+ params.cpos = 0;
+ params.cmdname = params.tokens[params.cpos++].c_str();
+ params.cmdline = line_str.c_str();
unsigned int cmdhash = HashString(params.cmdname);
const Command* cmd = reinterpret_cast<const Command*>(mzHashTableLookup(cmdht, cmdhash,
- params.cmdname, CompareCommandNames, false));
+ const_cast<char*>(params.cmdname), CompareCommandNames,
+ false));
if (cmd == nullptr) {
fprintf(stderr, "unexpected command [%s]\n", params.cmdname);
@@ -1481,12 +1491,10 @@
rc = 0;
pbiudone:
- if (params.fd != -1) {
- if (fsync(params.fd) == -1) {
- fprintf(stderr, "fsync failed: %s\n", strerror(errno));
- }
- close(params.fd);
+ if (fsync(params.fd) == -1) {
+ fprintf(stderr, "fsync failed: %s\n", strerror(errno));
}
+ // params.fd will be automatically closed because of the fd_holder above.
// Only delete the stash if the update cannot be resumed, or it's
// a verification run and we created the stash.
@@ -1526,6 +1534,9 @@
// - (version 2+ only) load the given source range and stash
// the data in the given slot of the stash table.
//
+// free <stash_id>
+// - (version 3+ only) free the given stash data.
+//
// The creator of the transfer list will guarantee that no block
// is read (ie, used as the source for a patch or move) after it
// has been written.
@@ -1692,7 +1703,7 @@
}
if (fh.pread(buffer, BLOCKSIZE, (off64_t)j * BLOCKSIZE) != BLOCKSIZE) {
- ErrorAbort(state, "failed to recover %s (block %d): %s", filename->data,
+ ErrorAbort(state, "failed to recover %s (block %zu): %s", filename->data,
j, strerror(errno));
return StringValue(strdup(""));
}