summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--etc/init.rc7
-rw-r--r--recovery.cpp104
-rw-r--r--uncrypt/uncrypt.c23
-rw-r--r--updater/blockimg.c254
4 files changed, 329 insertions, 59 deletions
diff --git a/etc/init.rc b/etc/init.rc
index 8ff728097..c78a44a2a 100644
--- a/etc/init.rc
+++ b/etc/init.rc
@@ -46,7 +46,6 @@ on fs
write /sys/class/android_usb/android0/iProduct ${ro.product.model}
write /sys/class/android_usb/android0/iSerial ${ro.serialno}
-
on boot
ifup lo
hostname localhost
@@ -58,6 +57,9 @@ on boot
on load_all_props_action
load_all_props
+on firmware_mounts_complete
+ rm /dev/.booting
+
# Mount filesystems and start core system services.
on late-init
trigger early-fs
@@ -70,6 +72,9 @@ on late-init
# issued fs triggers have completed.
trigger load_all_props_action
+ # Remove a file to wake up anything waiting for firmware
+ trigger firmware_mounts_complete
+
trigger early-boot
trigger boot
diff --git a/recovery.cpp b/recovery.cpp
index e1a2a9678..1d22b248a 100644
--- a/recovery.cpp
+++ b/recovery.cpp
@@ -25,6 +25,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <sys/klog.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
@@ -76,9 +77,14 @@ static const char *CACHE_ROOT = "/cache";
static const char *SDCARD_ROOT = "/sdcard";
static const char *TEMPORARY_LOG_FILE = "/tmp/recovery.log";
static const char *TEMPORARY_INSTALL_FILE = "/tmp/last_install";
+static const char *LAST_KMSG_FILE = "/cache/recovery/last_kmsg";
+#define KLOG_DEFAULT_LEN (64 * 1024)
#define KEEP_LOG_COUNT 10
+// Number of lines per page when displaying a file on screen
+#define LINES_PER_PAGE 30
+
RecoveryUI* ui = NULL;
char* locale = NULL;
char recovery_version[PROPERTY_VALUE_MAX+1];
@@ -259,6 +265,44 @@ set_sdcard_update_bootloader_message() {
set_bootloader_message(&boot);
}
+// read from kernel log into buffer and write out to file
+static void
+save_kernel_log(const char *destination) {
+ int n;
+ char *buffer;
+ int klog_buf_len;
+ FILE *log;
+
+ klog_buf_len = klogctl(KLOG_SIZE_BUFFER, 0, 0);
+ if (klog_buf_len <= 0) {
+ LOGE("Error getting klog size (%s), using default\n", strerror(errno));
+ klog_buf_len = KLOG_DEFAULT_LEN;
+ }
+
+ buffer = (char *)malloc(klog_buf_len);
+ if (!buffer) {
+ LOGE("Can't alloc %d bytes for klog buffer\n", klog_buf_len);
+ return;
+ }
+
+ n = klogctl(KLOG_READ_ALL, buffer, klog_buf_len);
+ if (n < 0) {
+ LOGE("Error in reading klog (%s)\n", strerror(errno));
+ free(buffer);
+ return;
+ }
+
+ log = fopen_path(destination, "w");
+ if (log == NULL) {
+ LOGE("Can't open %s\n", destination);
+ free(buffer);
+ return;
+ }
+ fwrite(buffer, n, 1, log);
+ check_and_fclose(log, destination);
+ free(buffer);
+}
+
// How much of the temp log we have copied to the copy in cache.
static long tmplog_offset = 0;
@@ -306,8 +350,11 @@ copy_logs() {
copy_log_file(TEMPORARY_LOG_FILE, LOG_FILE, true);
copy_log_file(TEMPORARY_LOG_FILE, LAST_LOG_FILE, false);
copy_log_file(TEMPORARY_INSTALL_FILE, LAST_INSTALL_FILE, false);
+ save_kernel_log(LAST_KMSG_FILE);
chmod(LOG_FILE, 0600);
chown(LOG_FILE, 1000, 1000); // system user
+ chmod(LAST_KMSG_FILE, 0600);
+ chown(LAST_KMSG_FILE, 1000, 1000); // system user
chmod(LAST_LOG_FILE, 0640);
chmod(LAST_INSTALL_FILE, 0644);
sync();
@@ -681,29 +728,71 @@ static void file_to_ui(const char* fn) {
}
char line[1024];
int ct = 0;
+ int key = 0;
redirect_stdio("/dev/null");
while(fgets(line, sizeof(line), fp) != NULL) {
ui->Print("%s", line);
ct++;
- if (ct % 30 == 0) {
+ if (ct % LINES_PER_PAGE == 0) {
// give the user time to glance at the entries
- ui->WaitKey();
+ key = ui->WaitKey();
+
+ if (key == KEY_POWER) {
+ break;
+ }
+
+ if (key == KEY_VOLUMEUP) {
+ // Go back by seeking to the beginning and dumping ct - n
+ // lines. It's ugly, but this way we don't need to store
+ // the previous offsets. The files we're dumping here aren't
+ // expected to be very large.
+ int i;
+
+ ct -= 2 * LINES_PER_PAGE;
+ if (ct < 0) {
+ ct = 0;
+ }
+ fseek(fp, 0, SEEK_SET);
+ for (i = 0; i < ct; i++) {
+ fgets(line, sizeof(line), fp);
+ }
+ ui->Print("^^^^^^^^^^\n");
+ }
}
}
+
+ // If the user didn't abort, then give the user time to glance at
+ // the end of the log, sorry, no rewind here
+ if (key != KEY_POWER) {
+ ui->Print("\n--END-- (press any key)\n");
+ ui->WaitKey();
+ }
+
redirect_stdio(TEMPORARY_LOG_FILE);
fclose(fp);
}
static void choose_recovery_file(Device* device) {
- int i;
+ unsigned int i;
+ unsigned int n;
static const char** title_headers = NULL;
char *filename;
const char* headers[] = { "Select file to view",
"",
NULL };
- char* entries[KEEP_LOG_COUNT + 2];
+ // "Go back" + LAST_KMSG_FILE + KEEP_LOG_COUNT + terminating NULL entry
+ char* entries[KEEP_LOG_COUNT + 3];
memset(entries, 0, sizeof(entries));
+ n = 0;
+ entries[n++] = strdup("Go back");
+
+ // Add kernel kmsg file if available
+ if ((ensure_path_mounted(LAST_KMSG_FILE) == 0) && (access(LAST_KMSG_FILE, R_OK) == 0)) {
+ entries[n++] = strdup(LAST_KMSG_FILE);
+ }
+
+ // Add LAST_LOG_FILE + LAST_LOG_FILE.x
for (i = 0; i < KEEP_LOG_COUNT; i++) {
char *filename;
if (asprintf(&filename, (i==0) ? LAST_LOG_FILE : (LAST_LOG_FILE ".%d"), i) == -1) {
@@ -712,13 +801,12 @@ static void choose_recovery_file(Device* device) {
}
if ((ensure_path_mounted(filename) != 0) || (access(filename, R_OK) == -1)) {
free(filename);
- entries[i+1] = NULL;
+ entries[n++] = NULL;
break;
}
- entries[i+1] = filename;
+ entries[n++] = filename;
}
- entries[0] = strdup("Go back");
title_headers = prepend_title((const char**)headers);
while(1) {
@@ -727,7 +815,7 @@ static void choose_recovery_file(Device* device) {
file_to_ui(entries[chosen_item]);
}
- for (i = 0; i < KEEP_LOG_COUNT + 1; i++) {
+ for (i = 0; i < (sizeof(entries) / sizeof(*entries)); i++) {
free(entries[i]);
}
}
diff --git a/uncrypt/uncrypt.c b/uncrypt/uncrypt.c
index 189fa57e1..7fb0989a7 100644
--- a/uncrypt/uncrypt.c
+++ b/uncrypt/uncrypt.c
@@ -164,7 +164,12 @@ char* parse_recovery_command_file()
if (f == NULL) {
return NULL;
}
- FILE* fo = fopen(RECOVERY_COMMAND_FILE_TMP, "w");
+ int fd = open(RECOVERY_COMMAND_FILE_TMP, O_WRONLY | O_CREAT | O_SYNC, S_IRUSR | S_IWUSR);
+ if (fd < 0) {
+ ALOGE("failed to open %s\n", RECOVERY_COMMAND_FILE_TMP);
+ return NULL;
+ }
+ FILE* fo = fdopen(fd, "w");
while (fgets(temp, sizeof(temp), f)) {
printf("read: %s", temp);
@@ -175,6 +180,7 @@ char* parse_recovery_command_file()
fputs(temp, fo);
}
fclose(f);
+ fsync(fd);
fclose(fo);
if (fn) {
@@ -190,7 +196,12 @@ int produce_block_map(const char* path, const char* map_file, const char* blk_de
struct stat sb;
int ret;
- FILE* mapf = fopen(map_file, "w");
+ int mapfd = open(map_file, O_WRONLY | O_CREAT | O_SYNC, S_IRUSR | S_IWUSR);
+ if (mapfd < 0) {
+ ALOGE("failed to open %s\n", map_file);
+ return -1;
+ }
+ FILE* mapf = fdopen(mapfd, "w");
ret = stat(path, &sb);
if (ret != 0) {
@@ -232,7 +243,7 @@ int produce_block_map(const char* path, const char* map_file, const char* blk_de
int wfd = -1;
if (encrypted) {
- wfd = open(blk_dev, O_WRONLY);
+ wfd = open(blk_dev, O_WRONLY | O_SYNC);
if (wfd < 0) {
ALOGE("failed to open fd for writing: %s\n", strerror(errno));
return -1;
@@ -302,9 +313,11 @@ int produce_block_map(const char* path, const char* map_file, const char* blk_de
fprintf(mapf, "%d %d\n", ranges[i*2], ranges[i*2+1]);
}
+ fsync(mapfd);
fclose(mapf);
close(fd);
if (encrypted) {
+ fsync(wfd);
close(wfd);
}
@@ -318,7 +331,7 @@ void wipe_misc() {
struct fstab_rec* v = &fstab->recs[i];
if (!v->mount_point) continue;
if (strcmp(v->mount_point, "/misc") == 0) {
- int fd = open(v->blk_device, O_WRONLY);
+ int fd = open(v->blk_device, O_WRONLY | O_SYNC);
uint8_t zeroes[1088]; // sizeof(bootloader_message) from recovery
memset(zeroes, 0, sizeof(zeroes));
@@ -333,7 +346,7 @@ void wipe_misc() {
written += w;
}
}
-
+ fsync(fd);
close(fd);
}
}
diff --git a/updater/blockimg.c b/updater/blockimg.c
index c3319c973..302689313 100644
--- a/updater/blockimg.c
+++ b/updater/blockimg.c
@@ -61,7 +61,7 @@ static RangeSet* parse_range(char* text) {
RangeSet* out = malloc(sizeof(RangeSet) + num * sizeof(int));
if (out == NULL) {
- fprintf(stderr, "failed to allocate range of %lu bytes\n",
+ fprintf(stderr, "failed to allocate range of %zu bytes\n",
sizeof(RangeSet) + num * sizeof(int));
exit(1);
}
@@ -245,6 +245,133 @@ static void* unzip_new_data(void* cookie) {
return NULL;
}
+// 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:
+//
+// <src_range> <tgt_range>
+//
+// The source range is loaded into the provided buffer, reallocating
+// it to make it larger if necessary. The target ranges are returned
+// in *tgt, if tgt is non-NULL.
+
+static void LoadSrcTgtVersion1(char* wordsave, RangeSet** tgt, int* src_blocks,
+ uint8_t** buffer, size_t* buffer_alloc, int fd) {
+ char* word;
+
+ word = strtok_r(NULL, " ", &wordsave);
+ RangeSet* src = parse_range(word);
+
+ if (tgt != NULL) {
+ word = strtok_r(NULL, " ", &wordsave);
+ *tgt = parse_range(word);
+ }
+
+ allocate(src->size * BLOCKSIZE, buffer, buffer_alloc);
+ size_t p = 0;
+ int i;
+ for (i = 0; i < src->count; ++i) {
+ check_lseek(fd, (off64_t)src->pos[i*2] * BLOCKSIZE, SEEK_SET);
+ size_t sz = (src->pos[i*2+1] - src->pos[i*2]) * BLOCKSIZE;
+ readblock(fd, *buffer+p, sz);
+ p += sz;
+ }
+
+ *src_blocks = src->size;
+ free(src);
+}
+
+static void MoveRange(uint8_t* dest, RangeSet* locs, const uint8_t* source) {
+ // source contains packed data, which we want to move to the
+ // locations given in *locs in the dest buffer. source and dest
+ // may be the same buffer.
+
+ int start = locs->size;
+ int i;
+ for (i = locs->count-1; i >= 0; --i) {
+ int blocks = locs->pos[i*2+1] - locs->pos[i*2];
+ start -= blocks;
+ memmove(dest + (locs->pos[i*2] * BLOCKSIZE), source + (start * BLOCKSIZE),
+ blocks * BLOCKSIZE);
+ }
+}
+
+// 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:
+//
+// <tgt_range> <src_block_count> <src_range>
+// (loads data from source image only)
+//
+// <tgt_range> <src_block_count> - <[stash_id:stash_range] ...>
+// (loads data from stashes only)
+//
+// <tgt_range> <src_block_count> <src_range> <src_loc> <[stash_id:stash_range] ...>
+// (loads data from both source image and stashes)
+//
+// On return, buffer is filled with the loaded source data (rearranged
+// and combined with stashed data as necessary). buffer may be
+// reallocated if needed to accommodate the source data. *tgt is the
+// target RangeSet. Any stashes required are taken from stash_table
+// and free()'d after being used.
+
+static void LoadSrcTgtVersion2(char* wordsave, RangeSet** tgt, int* src_blocks,
+ uint8_t** buffer, size_t* buffer_alloc, int fd,
+ uint8_t** stash_table) {
+ char* word;
+
+ if (tgt != NULL) {
+ word = strtok_r(NULL, " ", &wordsave);
+ *tgt = parse_range(word);
+ }
+
+ word = strtok_r(NULL, " ", &wordsave);
+ *src_blocks = strtol(word, NULL, 0);
+
+ allocate(*src_blocks * BLOCKSIZE, buffer, buffer_alloc);
+
+ word = strtok_r(NULL, " ", &wordsave);
+ if (word[0] == '-' && word[1] == '\0') {
+ // no source ranges, only stashes
+ } else {
+ RangeSet* src = parse_range(word);
+
+ size_t p = 0;
+ int i;
+ for (i = 0; i < src->count; ++i) {
+ check_lseek(fd, (off64_t)src->pos[i*2] * BLOCKSIZE, SEEK_SET);
+ size_t sz = (src->pos[i*2+1] - src->pos[i*2]) * BLOCKSIZE;
+ readblock(fd, *buffer+p, sz);
+ p += sz;
+ }
+ free(src);
+
+ word = strtok_r(NULL, " ", &wordsave);
+ if (word == NULL) {
+ // no stashes, only source range
+ return;
+ }
+
+ RangeSet* locs = parse_range(word);
+ MoveRange(*buffer, locs, *buffer);
+ }
+
+ while ((word = strtok_r(NULL, " ", &wordsave)) != NULL) {
+ // 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.
+ char* colonsave = NULL;
+ char* colon = strtok_r(word, ":", &colonsave);
+ int stash_id = strtol(colon, NULL, 0);
+ colon = strtok_r(NULL, ":", &colonsave);
+ RangeSet* locs = parse_range(colon);
+ MoveRange(*buffer, locs, stash_table[stash_id]);
+ free(stash_table[stash_id]);
+ stash_table[stash_id] = NULL;
+ free(locs);
+ }
+}
+
// args:
// - block device (or file) to modify in-place
// - transfer list (blob)
@@ -311,23 +438,33 @@ Value* BlockImageUpdateFn(const char* name, State* state, int argc, Expr* argv[]
// new [rangeset]
// - fill the blocks with data read from the new_data file
//
- // bsdiff patchstart patchlen [src rangeset] [tgt rangeset]
- // imgdiff patchstart patchlen [src rangeset] [tgt rangeset]
- // - read the source blocks, apply a patch, write result to
- // target blocks. bsdiff or imgdiff specifies the type of
- // patch.
- //
- // move [src rangeset] [tgt rangeset]
- // - copy data from source blocks to target blocks (no patch
- // needed; rangesets are the same size)
- //
// erase [rangeset]
// - mark the given blocks as empty
//
+ // move <...>
+ // bsdiff <patchstart> <patchlen> <...>
+ // imgdiff <patchstart> <patchlen> <...>
+ // - read the source blocks, apply a patch (or not in the
+ // case of move), write result to target blocks. bsdiff or
+ // imgdiff specifies the type of patch; move means no patch
+ // at all.
+ //
+ // The format of <...> differs between versions 1 and 2;
+ // see the LoadSrcTgtVersion{1,2}() functions for a
+ // description of what's expected.
+ //
+ // stash <stash_id> <src_range>
+ // - (version 2 only) load the given source range and stash
+ // the data in the given slot of the stash table.
+ //
// 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.
//
+ // In version 2, the creator will guarantee that a given stash is
+ // loaded (with a stash command) before it's used in a
+ // move/bsdiff/imgdiff command.
+ //
// Within one command the source and target ranges may overlap so
// in general we need to read the entire source into memory before
// writing anything to the target blocks.
@@ -379,12 +516,18 @@ Value* BlockImageUpdateFn(const char* name, State* state, int argc, Expr* argv[]
line = strtok_r(transfer_list, "\n", &linesave);
+ int version;
// first line in transfer list is the version number; currently
// there's only version 1.
- if (strcmp(line, "1") != 0) {
+ if (strcmp(line, "1") == 0) {
+ version = 1;
+ } else if (strcmp(line, "2") == 0) {
+ version = 2;
+ } else {
ErrorAbort(state, "unexpected transfer list version [%s]\n", line);
goto done;
}
+ printf("blockimg version is %d\n", version);
// second line in transfer list is the total number of blocks we
// expect to write.
@@ -394,33 +537,49 @@ Value* BlockImageUpdateFn(const char* name, State* state, int argc, Expr* argv[]
if (total_blocks == 0) ++total_blocks;
int blocks_so_far = 0;
+ uint8_t** stash_table = NULL;
+ if (version >= 2) {
+ // Next line is how many stash entries are needed simultaneously.
+ line = strtok_r(NULL, "\n", &linesave);
+ int stash_entries = strtol(line, NULL, 0);
+
+ stash_table = (uint8_t**) calloc(stash_entries, sizeof(uint8_t*));
+ if (stash_table == NULL) {
+ fprintf(stderr, "failed to allocate %d-entry stash table\n", stash_entries);
+ exit(1);
+ }
+
+ // Next line is the maximum number of blocks that will be
+ // stashed simultaneously. This could be used to verify that
+ // enough memory or scratch disk space is available.
+ line = strtok_r(NULL, "\n", &linesave);
+ int stash_max_blocks = strtol(line, NULL, 0);
+ }
+
uint8_t* buffer = NULL;
size_t buffer_alloc = 0;
// third and subsequent lines are all individual transfer commands.
for (line = strtok_r(NULL, "\n", &linesave); line;
line = strtok_r(NULL, "\n", &linesave)) {
+
char* style;
style = strtok_r(line, " ", &wordsave);
if (strcmp("move", style) == 0) {
- word = strtok_r(NULL, " ", &wordsave);
- RangeSet* src = parse_range(word);
- word = strtok_r(NULL, " ", &wordsave);
- RangeSet* tgt = parse_range(word);
+ RangeSet* tgt;
+ int src_blocks;
+ if (version == 1) {
+ LoadSrcTgtVersion1(wordsave, &tgt, &src_blocks,
+ &buffer, &buffer_alloc, fd);
+ } else if (version == 2) {
+ LoadSrcTgtVersion2(wordsave, &tgt, &src_blocks,
+ &buffer, &buffer_alloc, fd, stash_table);
+ }
- printf(" moving %d blocks\n", src->size);
+ printf(" moving %d blocks\n", src_blocks);
- allocate(src->size * BLOCKSIZE, &buffer, &buffer_alloc);
size_t p = 0;
- for (i = 0; i < src->count; ++i) {
- check_lseek(fd, (off64_t)src->pos[i*2] * BLOCKSIZE, SEEK_SET);
- size_t sz = (src->pos[i*2+1] - src->pos[i*2]) * BLOCKSIZE;
- readblock(fd, buffer+p, sz);
- p += sz;
- }
-
- p = 0;
for (i = 0; i < tgt->count; ++i) {
check_lseek(fd, (off64_t)tgt->pos[i*2] * BLOCKSIZE, SEEK_SET);
size_t sz = (tgt->pos[i*2+1] - tgt->pos[i*2]) * BLOCKSIZE;
@@ -432,9 +591,20 @@ Value* BlockImageUpdateFn(const char* name, State* state, int argc, Expr* argv[]
fprintf(cmd_pipe, "set_progress %.4f\n", (double)blocks_so_far / total_blocks);
fflush(cmd_pipe);
- free(src);
free(tgt);
+ } else if (strcmp("stash", style) == 0) {
+ word = strtok_r(NULL, " ", &wordsave);
+ int stash_id = strtol(word, NULL, 0);
+ int src_blocks;
+ size_t stash_alloc = 0;
+
+ // Even though the "stash" style only appears in version
+ // 2, the version 1 source loader happens to do exactly
+ // what we want to read data into the stash_table.
+ LoadSrcTgtVersion1(wordsave, NULL, &src_blocks,
+ stash_table + stash_id, &stash_alloc, fd);
+
} else if (strcmp("zero", style) == 0 ||
(DEBUG_ERASE && strcmp("erase", style) == 0)) {
word = strtok_r(NULL, " ", &wordsave);
@@ -493,23 +663,18 @@ Value* BlockImageUpdateFn(const char* name, State* state, int argc, Expr* argv[]
word = strtok_r(NULL, " ", &wordsave);
size_t patch_len = strtoul(word, NULL, 0);
- word = strtok_r(NULL, " ", &wordsave);
- RangeSet* src = parse_range(word);
- word = strtok_r(NULL, " ", &wordsave);
- RangeSet* tgt = parse_range(word);
-
- printf(" patching %d blocks to %d\n", src->size, tgt->size);
-
- // Read the source into memory.
- allocate(src->size * BLOCKSIZE, &buffer, &buffer_alloc);
- size_t p = 0;
- for (i = 0; i < src->count; ++i) {
- check_lseek(fd, (off64_t)src->pos[i*2] * BLOCKSIZE, SEEK_SET);
- size_t sz = (src->pos[i*2+1] - src->pos[i*2]) * BLOCKSIZE;
- readblock(fd, buffer+p, sz);
- p += sz;
+ RangeSet* tgt;
+ int src_blocks;
+ if (version == 1) {
+ LoadSrcTgtVersion1(wordsave, &tgt, &src_blocks,
+ &buffer, &buffer_alloc, fd);
+ } else if (version == 2) {
+ LoadSrcTgtVersion2(wordsave, &tgt, &src_blocks,
+ &buffer, &buffer_alloc, fd, stash_table);
}
+ printf(" patching %d blocks to %d\n", src_blocks, tgt->size);
+
Value patch_value;
patch_value.type = VAL_BLOB;
patch_value.size = patch_len;
@@ -523,11 +688,11 @@ Value* BlockImageUpdateFn(const char* name, State* state, int argc, Expr* argv[]
check_lseek(fd, (off64_t)tgt->pos[0] * BLOCKSIZE, SEEK_SET);
if (style[0] == 'i') { // imgdiff
- ApplyImagePatch(buffer, src->size * BLOCKSIZE,
+ ApplyImagePatch(buffer, src_blocks * BLOCKSIZE,
&patch_value,
&RangeSinkWrite, &rss, NULL, NULL);
} else {
- ApplyBSDiffPatch(buffer, src->size * BLOCKSIZE,
+ ApplyBSDiffPatch(buffer, src_blocks * BLOCKSIZE,
&patch_value, 0,
&RangeSinkWrite, &rss, NULL);
}
@@ -541,7 +706,6 @@ Value* BlockImageUpdateFn(const char* name, State* state, int argc, Expr* argv[]
fprintf(cmd_pipe, "set_progress %.4f\n", (double)blocks_so_far / total_blocks);
fflush(cmd_pipe);
- free(src);
free(tgt);
} else if (!DEBUG_ERASE && strcmp("erase", style) == 0) {
struct stat st;