am ff934d02: Merge "Fix Droid and animation color in recovery mode"

* commit 'ff934d0257909e9c5420815547a6e2ff89455314':
  Fix Droid and animation color in recovery mode
diff --git a/applypatch/applypatch.c b/applypatch/applypatch.c
index bfb9440..2c86e09 100644
--- a/applypatch/applypatch.c
+++ b/applypatch/applypatch.c
@@ -309,7 +309,7 @@
 // Save the contents of the given FileContents object under the given
 // filename.  Return 0 on success.
 int SaveFileContents(const char* filename, const FileContents* file) {
-    int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
+    int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC | O_SYNC, S_IRUSR | S_IWUSR);
     if (fd < 0) {
         printf("failed to open \"%s\" for write: %s\n",
                filename, strerror(errno));
@@ -324,8 +324,14 @@
         close(fd);
         return -1;
     }
-    fsync(fd);
-    close(fd);
+    if (fsync(fd) != 0) {
+        printf("fsync of \"%s\" failed: %s\n", filename, strerror(errno));
+        return -1;
+    }
+    if (close(fd) != 0) {
+        printf("close of \"%s\" failed: %s\n", filename, strerror(errno));
+        return -1;
+    }
 
     if (chmod(filename, file->st.st_mode) != 0) {
         printf("chmod of \"%s\" failed: %s\n", filename, strerror(errno));
@@ -408,7 +414,7 @@
         {
             size_t start = 0;
             int success = 0;
-            int fd = open(partition, O_RDWR);
+            int fd = open(partition, O_RDWR | O_SYNC);
             if (fd < 0) {
                 printf("failed to open %s: %s\n", partition, strerror(errno));
                 return -1;
@@ -433,7 +439,22 @@
                     }
                     start += written;
                 }
-                fsync(fd);
+                if (fsync(fd) != 0) {
+                   printf("failed to sync to %s (%s)\n",
+                          partition, strerror(errno));
+                   return -1;
+                }
+                if (close(fd) != 0) {
+                   printf("failed to close %s (%s)\n",
+                          partition, strerror(errno));
+                   return -1;
+                }
+                fd = open(partition, O_RDONLY);
+                if (fd < 0) {
+                   printf("failed to reopen %s for verify (%s)\n",
+                          partition, strerror(errno));
+                   return -1;
+                }
 
                 // drop caches so our subsequent verification read
                 // won't just be reading the cache.
@@ -919,7 +940,8 @@
             strcpy(outname, target_filename);
             strcat(outname, ".patch");
 
-            output = open(outname, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
+            output = open(outname, O_WRONLY | O_CREAT | O_TRUNC | O_SYNC,
+                S_IRUSR | S_IWUSR);
             if (output < 0) {
                 printf("failed to open output file %s: %s\n",
                        outname, strerror(errno));
@@ -950,8 +972,14 @@
         }
 
         if (output >= 0) {
-            fsync(output);
-            close(output);
+            if (fsync(output) != 0) {
+                printf("failed to fsync file \"%s\" (%s)\n", outname, strerror(errno));
+                result = 1;
+            }
+            if (close(output) != 0) {
+                printf("failed to close file \"%s\" (%s)\n", outname, strerror(errno));
+                result = 1;
+            }
         }
 
         if (result != 0) {
diff --git a/default_device.cpp b/default_device.cpp
index a25f05f..97806ac 100644
--- a/default_device.cpp
+++ b/default_device.cpp
@@ -31,6 +31,7 @@
                                "wipe cache partition",
                                "reboot to bootloader",
                                "power down",
+                               "view recovery logs",
                                NULL };
 
 class DefaultDevice : public Device {
@@ -69,6 +70,7 @@
           case 3: return WIPE_CACHE;
           case 4: return REBOOT_BOOTLOADER;
           case 5: return SHUTDOWN;
+          case 6: return READ_RECOVERY_LASTLOG;
           default: return NO_ACTION;
         }
     }
diff --git a/device.h b/device.h
index 57ec3fc..8ff4ec0 100644
--- a/device.h
+++ b/device.h
@@ -68,7 +68,7 @@
     enum BuiltinAction { NO_ACTION, REBOOT, APPLY_EXT,
                          APPLY_CACHE,   // APPLY_CACHE is deprecated; has no effect
                          APPLY_ADB_SIDELOAD, WIPE_DATA, WIPE_CACHE,
-                         REBOOT_BOOTLOADER, SHUTDOWN };
+                         REBOOT_BOOTLOADER, SHUTDOWN, READ_RECOVERY_LASTLOG };
 
     // Perform a recovery action selected from the menu.
     // 'menu_position' will be the item number of the selected menu
diff --git a/etc/init.rc b/etc/init.rc
index 1b402e2..c78a44a 100644
--- a/etc/init.rc
+++ b/etc/init.rc
@@ -30,6 +30,7 @@
     chmod 0775 /tmp
 
     write /proc/sys/kernel/panic_on_oops 1
+    write /proc/sys/vm/max_map_count 1000000
 
 on fs
     mkdir /dev/usb-ffs 0770 shell shell
@@ -45,7 +46,6 @@
     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
@@ -57,6 +57,9 @@
 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
@@ -69,6 +72,9 @@
     # 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/minui/graphics_fbdev.c b/minui/graphics_fbdev.c
index ecd40c3..4a5b5b5 100644
--- a/minui/graphics_fbdev.c
+++ b/minui/graphics_fbdev.c
@@ -180,6 +180,18 @@
 
 static gr_surface fbdev_flip(minui_backend* backend __unused) {
     if (double_buffered) {
+#if defined(RECOVERY_BGRA)
+        // In case of BGRA, do some byte swapping
+        unsigned int idx;
+        unsigned char tmp;
+        unsigned char* ucfb_vaddr = (unsigned char*)gr_draw->data;
+        for (idx = 0 ; idx < (gr_draw->height * gr_draw->row_bytes);
+                idx += 4) {
+            tmp = ucfb_vaddr[idx];
+            ucfb_vaddr[idx    ] = ucfb_vaddr[idx + 2];
+            ucfb_vaddr[idx + 2] = tmp;
+        }
+#endif
         // Change gr_draw to point to the buffer currently displayed,
         // then flip the driver so we're displaying the other buffer
         // instead.
diff --git a/minzip/Zip.c b/minzip/Zip.c
index aec35e3..579e531 100644
--- a/minzip/Zip.c
+++ b/minzip/Zip.c
@@ -1063,7 +1063,8 @@
                     setfscreatecon(secontext);
                 }
 
-                int fd = creat(targetFile, UNZIP_FILEMODE);
+                int fd = open(targetFile, O_CREAT|O_WRONLY|O_TRUNC|O_SYNC
+                        , UNZIP_FILEMODE);
 
                 if (secontext) {
                     freecon(secontext);
@@ -1078,7 +1079,12 @@
                 }
 
                 bool ok = mzExtractZipEntryToFile(pArchive, pEntry, fd);
-                close(fd);
+                if (ok) {
+                    ok = (fsync(fd) == 0);
+                }
+                if (close(fd) != 0) {
+                    ok = false;
+                }
                 if (!ok) {
                     LOGE("Error extracting \"%s\"\n", targetFile);
                     ok = false;
diff --git a/recovery.cpp b/recovery.cpp
index d8756d7..e42474e 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,6 +77,13 @@
 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;
@@ -166,6 +174,12 @@
     return (property_get("ro.debuggable", value, NULL) == 1 && value[0] == '1');
 }
 
+static void redirect_stdio(const char* filename) {
+    // If these fail, there's not really anywhere to complain...
+    freopen(filename, "a", stdout); setbuf(stdout, NULL);
+    freopen(filename, "a", stderr); setbuf(stderr, NULL);
+}
+
 // close a file, log an error if the error indicator is set
 static void
 check_and_fclose(FILE *fp, const char *name) {
@@ -256,6 +270,44 @@
     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;
 
@@ -303,8 +355,11 @@
     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();
@@ -670,6 +725,106 @@
     ui->Print("Data wipe complete.\n");
 }
 
+static void file_to_ui(const char* fn) {
+    FILE *fp = fopen_path(fn, "re");
+    if (fp == NULL) {
+        ui->Print("  Unable to open %s: %s\n", fn, strerror(errno));
+        return;
+    }
+    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 % LINES_PER_PAGE == 0) {
+            // give the user time to glance at the entries
+            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) {
+    unsigned int i;
+    unsigned int n;
+    static const char** title_headers = NULL;
+    char *filename;
+    const char* headers[] = { "Select file to view",
+                              "",
+                              NULL };
+    // "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) {
+            // memory allocation failure - return early. Should never happen.
+            return;
+        }
+        if ((ensure_path_mounted(filename) != 0) || (access(filename, R_OK) == -1)) {
+            free(filename);
+            entries[n++] = NULL;
+            break;
+        }
+        entries[n++] = filename;
+    }
+
+    title_headers = prepend_title((const char**)headers);
+
+    while(1) {
+        int chosen_item = get_menu_selection(title_headers, entries, 1, 0, device);
+        if (chosen_item == 0) break;
+        file_to_ui(entries[chosen_item]);
+    }
+
+    for (i = 0; i < (sizeof(entries) / sizeof(*entries)); i++) {
+        free(entries[i]);
+    }
+}
+
 // Return REBOOT, SHUTDOWN, or REBOOT_BOOTLOADER.  Returning NO_ACTION
 // means to take the default, which is to reboot or shutdown depending
 // on if the --shutdown_after flag was passed to recovery.
@@ -765,6 +920,10 @@
                 ui->Print("\nAPPLY_CACHE is deprecated.\n");
                 break;
 
+            case Device::READ_RECOVERY_LASTLOG:
+                choose_recovery_file(device);
+                break;
+
             case Device::APPLY_ADB_SIDELOAD:
                 status = apply_from_adb(ui, &wipe_cache, TEMPORARY_INSTALL_FILE);
                 if (status >= 0) {
@@ -829,9 +988,7 @@
 main(int argc, char **argv) {
     time_t start = time(NULL);
 
-    // If these fail, there's not really anywhere to complain...
-    freopen(TEMPORARY_LOG_FILE, "a", stdout); setbuf(stdout, NULL);
-    freopen(TEMPORARY_LOG_FILE, "a", stderr); setbuf(stderr, NULL);
+    redirect_stdio(TEMPORARY_LOG_FILE);
 
     // If this binary is started with the single argument "--adbd",
     // instead of being the normal recovery binary, it turns into kind
@@ -849,7 +1006,7 @@
 
     load_volume_table();
     ensure_path_mounted(LAST_LOG_FILE);
-    rotate_last_logs(10);
+    rotate_last_logs(KEEP_LOG_COUNT);
     get_args(&argc, &argv);
 
     const char *send_intent = NULL;
diff --git a/uncrypt/uncrypt.c b/uncrypt/uncrypt.c
index b90bd6b..aa75210 100644
--- a/uncrypt/uncrypt.c
+++ b/uncrypt/uncrypt.c
@@ -166,7 +166,12 @@
     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);
@@ -177,6 +182,7 @@
         fputs(temp, fo);
     }
     fclose(f);
+    fsync(fd);
     fclose(fo);
 
     if (fn) {
@@ -192,7 +198,12 @@
     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) {
@@ -234,7 +245,7 @@
 
     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;
@@ -304,9 +315,11 @@
         fprintf(mapf, "%d %d\n", ranges[i*2], ranges[i*2+1]);
     }
 
+    fsync(mapfd);
     fclose(mapf);
     close(fd);
     if (encrypted) {
+        fsync(wfd);
         close(wfd);
     }
 
@@ -320,7 +333,7 @@
         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));
 
@@ -335,7 +348,7 @@
                     written += w;
                 }
             }
-
+            fsync(fd);
             close(fd);
         }
     }
diff --git a/updater/Android.mk b/updater/Android.mk
index c73cdc0..ff02a33 100644
--- a/updater/Android.mk
+++ b/updater/Android.mk
@@ -34,6 +34,16 @@
 LOCAL_STATIC_LIBRARIES += libmincrypt libbz
 LOCAL_STATIC_LIBRARIES += libcutils liblog libstdc++ libc
 LOCAL_STATIC_LIBRARIES += libselinux
+tune2fs_static_libraries := \
+ libext2_com_err \
+ libext2_blkid \
+ libext2_quota \
+ libext2_uuid_static \
+ libext2_e2p \
+ libext2fs
+LOCAL_STATIC_LIBRARIES += libtune2fs $(tune2fs_static_libraries)
+
+LOCAL_C_INCLUDES += external/e2fsprogs/misc
 LOCAL_C_INCLUDES += $(LOCAL_PATH)/..
 
 # Each library in TARGET_RECOVERY_UPDATER_LIBS should have a function
diff --git a/updater/blockimg.c b/updater/blockimg.c
index c3319c9..3026893 100644
--- a/updater/blockimg.c
+++ b/updater/blockimg.c
@@ -61,7 +61,7 @@
 
     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 @@
     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 @@
     //    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 @@
 
     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 @@
     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);
-
-            printf("  moving %d blocks\n", src->size);
-
-            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);
             }
 
-            p = 0;
+            printf("  moving %d blocks\n", src_blocks);
+
+            size_t 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 @@
             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 @@
             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 @@
             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 @@
             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;
diff --git a/updater/install.c b/updater/install.c
index dad0d08..2b2ffb0 100644
--- a/updater/install.c
+++ b/updater/install.c
@@ -46,12 +46,33 @@
 #include "mtdutils/mtdutils.h"
 #include "updater.h"
 #include "install.h"
+#include "tune2fs.h"
 
 #ifdef USE_EXT4
 #include "make_ext4fs.h"
 #include "wipe.h"
 #endif
 
+void uiPrint(State* state, char* buffer) {
+    char* line = strtok(buffer, "\n");
+    UpdaterInfo* ui = (UpdaterInfo*)(state->cookie);
+    while (line) {
+        fprintf(ui->cmd_pipe, "ui_print %s\n", line);
+        line = strtok(NULL, "\n");
+    }
+    fprintf(ui->cmd_pipe, "ui_print\n");
+}
+
+__attribute__((__format__(printf, 2, 3))) __nonnull((2))
+void uiPrintf(State* state, const char* format, ...) {
+    char error_msg[1024];
+    va_list ap;
+    va_start(ap, format);
+    vsnprintf(error_msg, sizeof(error_msg), format, ap);
+    va_end(ap);
+    uiPrint(state, error_msg);
+}
+
 // Take a sha-1 digest and return it as a newly-allocated hex string.
 char* PrintSha1(const uint8_t* digest) {
     char* buffer = malloc(SHA_DIGEST_SIZE*2 + 1);
@@ -71,16 +92,27 @@
 //    fs_type="ext4"   partition_type="EMMC"    location=device
 Value* MountFn(const char* name, State* state, int argc, Expr* argv[]) {
     char* result = NULL;
-    if (argc != 4) {
-        return ErrorAbort(state, "%s() expects 4 args, got %d", name, argc);
+    if (argc != 4 && argc != 5) {
+        return ErrorAbort(state, "%s() expects 4-5 args, got %d", name, argc);
     }
     char* fs_type;
     char* partition_type;
     char* location;
     char* mount_point;
-    if (ReadArgs(state, argv, 4, &fs_type, &partition_type,
+    char* mount_options;
+    bool has_mount_options;
+    if (argc == 5) {
+        has_mount_options = true;
+        if (ReadArgs(state, argv, 5, &fs_type, &partition_type,
+                 &location, &mount_point, &mount_options) < 0) {
+            return NULL;
+        }
+    } else {
+        has_mount_options = false;
+        if (ReadArgs(state, argv, 4, &fs_type, &partition_type,
                  &location, &mount_point) < 0) {
-        return NULL;
+            return NULL;
+        }
     }
 
     if (strlen(fs_type) == 0) {
@@ -120,13 +152,13 @@
         const MtdPartition* mtd;
         mtd = mtd_find_partition_by_name(location);
         if (mtd == NULL) {
-            printf("%s: no mtd partition named \"%s\"",
+            uiPrintf(state, "%s: no mtd partition named \"%s\"",
                     name, location);
             result = strdup("");
             goto done;
         }
         if (mtd_mount_partition(mtd, mount_point, fs_type, 0 /* rw */) != 0) {
-            printf("mtd mount of %s failed: %s\n",
+            uiPrintf(state, "mtd mount of %s failed: %s\n",
                     location, strerror(errno));
             result = strdup("");
             goto done;
@@ -134,8 +166,9 @@
         result = mount_point;
     } else {
         if (mount(location, mount_point, fs_type,
-                  MS_NOATIME | MS_NODEV | MS_NODIRATIME, "") < 0) {
-            printf("%s: failed to mount %s at %s: %s\n",
+                  MS_NOATIME | MS_NODEV | MS_NODIRATIME,
+                  has_mount_options ? mount_options : "") < 0) {
+            uiPrintf(state, "%s: failed to mount %s at %s: %s\n",
                     name, location, mount_point, strerror(errno));
             result = strdup("");
         } else {
@@ -148,6 +181,7 @@
     free(partition_type);
     free(location);
     if (result != mount_point) free(mount_point);
+    if (has_mount_options) free(mount_options);
     return StringValue(result);
 }
 
@@ -198,10 +232,14 @@
     scan_mounted_volumes();
     const MountedVolume* vol = find_mounted_volume_by_mount_point(mount_point);
     if (vol == NULL) {
-        printf("unmount of %s failed; no such volume\n", mount_point);
+        uiPrintf(state, "unmount of %s failed; no such volume\n", mount_point);
         result = strdup("");
     } else {
-        unmount_mounted_volume(vol);
+        int ret = unmount_mounted_volume(vol);
+        if (ret != 0) {
+           uiPrintf(state, "unmount of %s failed (%d): %s\n",
+                    mount_point, ret, strerror(errno));
+        }
         result = mount_point;
     }
 
@@ -358,6 +396,9 @@
     if (make_parents(dst_name) != 0) {
         ErrorAbort(state, "Creating parent of %s failed, error %s",
           dst_name, strerror(errno));
+    } else if (access(dst_name, F_OK) == 0 && access(src_name, F_OK) != 0) {
+        // File was already moved
+        result = dst_name;
     } else if (rename(src_name, dst_name) != 0) {
         ErrorAbort(state, "Rename of %s to %s failed, error %s",
           src_name, dst_name, strerror(errno));
@@ -630,7 +671,7 @@
     uint64_t capabilities;
 };
 
-static struct perm_parsed_args ParsePermArgs(int argc, char** args) {
+static struct perm_parsed_args ParsePermArgs(State * state, int argc, char** args) {
     int i;
     struct perm_parsed_args parsed;
     int bad = 0;
@@ -645,7 +686,7 @@
                 parsed.uid = uid;
                 parsed.has_uid = true;
             } else {
-                printf("ParsePermArgs: invalid UID \"%s\"\n", args[i + 1]);
+                uiPrintf(state, "ParsePermArgs: invalid UID \"%s\"\n", args[i + 1]);
                 bad++;
             }
             continue;
@@ -656,7 +697,7 @@
                 parsed.gid = gid;
                 parsed.has_gid = true;
             } else {
-                printf("ParsePermArgs: invalid GID \"%s\"\n", args[i + 1]);
+                uiPrintf(state, "ParsePermArgs: invalid GID \"%s\"\n", args[i + 1]);
                 bad++;
             }
             continue;
@@ -667,7 +708,7 @@
                 parsed.mode = mode;
                 parsed.has_mode = true;
             } else {
-                printf("ParsePermArgs: invalid mode \"%s\"\n", args[i + 1]);
+                uiPrintf(state, "ParsePermArgs: invalid mode \"%s\"\n", args[i + 1]);
                 bad++;
             }
             continue;
@@ -678,7 +719,7 @@
                 parsed.dmode = mode;
                 parsed.has_dmode = true;
             } else {
-                printf("ParsePermArgs: invalid dmode \"%s\"\n", args[i + 1]);
+                uiPrintf(state, "ParsePermArgs: invalid dmode \"%s\"\n", args[i + 1]);
                 bad++;
             }
             continue;
@@ -689,7 +730,7 @@
                 parsed.fmode = mode;
                 parsed.has_fmode = true;
             } else {
-                printf("ParsePermArgs: invalid fmode \"%s\"\n", args[i + 1]);
+                uiPrintf(state, "ParsePermArgs: invalid fmode \"%s\"\n", args[i + 1]);
                 bad++;
             }
             continue;
@@ -700,7 +741,7 @@
                 parsed.capabilities = capabilities;
                 parsed.has_capabilities = true;
             } else {
-                printf("ParsePermArgs: invalid capabilities \"%s\"\n", args[i + 1]);
+                uiPrintf(state, "ParsePermArgs: invalid capabilities \"%s\"\n", args[i + 1]);
                 bad++;
             }
             continue;
@@ -710,7 +751,7 @@
                 parsed.selabel = args[i+1];
                 parsed.has_selabel = true;
             } else {
-                printf("ParsePermArgs: invalid selabel \"%s\"\n", args[i + 1]);
+                uiPrintf(state, "ParsePermArgs: invalid selabel \"%s\"\n", args[i + 1]);
                 bad++;
             }
             continue;
@@ -727,71 +768,71 @@
 }
 
 static int ApplyParsedPerms(
+        State * state,
         const char* filename,
         const struct stat *statptr,
         struct perm_parsed_args parsed)
 {
     int bad = 0;
 
+    if (parsed.has_selabel) {
+        if (lsetfilecon(filename, parsed.selabel) != 0) {
+            uiPrintf(state, "ApplyParsedPerms: lsetfilecon of %s to %s failed: %s\n",
+                    filename, parsed.selabel, strerror(errno));
+            bad++;
+        }
+    }
+
     /* ignore symlinks */
     if (S_ISLNK(statptr->st_mode)) {
-        return 0;
+        return bad;
     }
 
     if (parsed.has_uid) {
         if (chown(filename, parsed.uid, -1) < 0) {
-            printf("ApplyParsedPerms: chown of %s to %d failed: %s\n",
-                   filename, parsed.uid, strerror(errno));
+            uiPrintf(state, "ApplyParsedPerms: chown of %s to %d failed: %s\n",
+                    filename, parsed.uid, strerror(errno));
             bad++;
         }
     }
 
     if (parsed.has_gid) {
         if (chown(filename, -1, parsed.gid) < 0) {
-            printf("ApplyParsedPerms: chgrp of %s to %d failed: %s\n",
-                   filename, parsed.gid, strerror(errno));
+            uiPrintf(state, "ApplyParsedPerms: chgrp of %s to %d failed: %s\n",
+                    filename, parsed.gid, strerror(errno));
             bad++;
         }
     }
 
     if (parsed.has_mode) {
         if (chmod(filename, parsed.mode) < 0) {
-            printf("ApplyParsedPerms: chmod of %s to %d failed: %s\n",
-                   filename, parsed.mode, strerror(errno));
+            uiPrintf(state, "ApplyParsedPerms: chmod of %s to %d failed: %s\n",
+                    filename, parsed.mode, strerror(errno));
             bad++;
         }
     }
 
     if (parsed.has_dmode && S_ISDIR(statptr->st_mode)) {
         if (chmod(filename, parsed.dmode) < 0) {
-            printf("ApplyParsedPerms: chmod of %s to %d failed: %s\n",
-                   filename, parsed.dmode, strerror(errno));
+            uiPrintf(state, "ApplyParsedPerms: chmod of %s to %d failed: %s\n",
+                    filename, parsed.dmode, strerror(errno));
             bad++;
         }
     }
 
     if (parsed.has_fmode && S_ISREG(statptr->st_mode)) {
         if (chmod(filename, parsed.fmode) < 0) {
-            printf("ApplyParsedPerms: chmod of %s to %d failed: %s\n",
+            uiPrintf(state, "ApplyParsedPerms: chmod of %s to %d failed: %s\n",
                    filename, parsed.fmode, strerror(errno));
             bad++;
         }
     }
 
-    if (parsed.has_selabel) {
-        // TODO: Don't silently ignore ENOTSUP
-        if (lsetfilecon(filename, parsed.selabel) && (errno != ENOTSUP)) {
-            printf("ApplyParsedPerms: lsetfilecon of %s to %s failed: %s\n",
-                   filename, parsed.selabel, strerror(errno));
-            bad++;
-        }
-    }
-
     if (parsed.has_capabilities && S_ISREG(statptr->st_mode)) {
         if (parsed.capabilities == 0) {
             if ((removexattr(filename, XATTR_NAME_CAPS) == -1) && (errno != ENODATA)) {
                 // Report failure unless it's ENODATA (attribute not set)
-                printf("ApplyParsedPerms: removexattr of %s to %" PRIx64 " failed: %s\n",
+                uiPrintf(state, "ApplyParsedPerms: removexattr of %s to %" PRIx64 " failed: %s\n",
                        filename, parsed.capabilities, strerror(errno));
                 bad++;
             }
@@ -804,8 +845,8 @@
             cap_data.data[1].permitted = (uint32_t) (parsed.capabilities >> 32);
             cap_data.data[1].inheritable = 0;
             if (setxattr(filename, XATTR_NAME_CAPS, &cap_data, sizeof(cap_data), 0) < 0) {
-                printf("ApplyParsedPerms: setcap of %s to %" PRIx64 " failed: %s\n",
-                       filename, parsed.capabilities, strerror(errno));
+                uiPrintf(state, "ApplyParsedPerms: setcap of %s to %" PRIx64 " failed: %s\n",
+                        filename, parsed.capabilities, strerror(errno));
                 bad++;
             }
         }
@@ -817,10 +858,11 @@
 // nftw doesn't allow us to pass along context, so we need to use
 // global variables.  *sigh*
 static struct perm_parsed_args recursive_parsed_args;
+static State* recursive_state;
 
 static int do_SetMetadataRecursive(const char* filename, const struct stat *statptr,
         int fileflags, struct FTW *pfwt) {
-    return ApplyParsedPerms(filename, statptr, recursive_parsed_args);
+    return ApplyParsedPerms(recursive_state, filename, statptr, recursive_parsed_args);
 }
 
 static Value* SetMetadataFn(const char* name, State* state, int argc, Expr* argv[]) {
@@ -845,14 +887,16 @@
         goto done;
     }
 
-    struct perm_parsed_args parsed = ParsePermArgs(argc, args);
+    struct perm_parsed_args parsed = ParsePermArgs(state, argc, args);
 
     if (recursive) {
         recursive_parsed_args = parsed;
+        recursive_state = state;
         bad += nftw(args[0], do_SetMetadataRecursive, 30, FTW_CHDIR | FTW_DEPTH | FTW_PHYS);
         memset(&recursive_parsed_args, 0, sizeof(recursive_parsed_args));
+        recursive_state = NULL;
     } else {
-        bad += ApplyParsedPerms(args[0], &sb, parsed);
+        bad += ApplyParsedPerms(state, args[0], &sb, parsed);
     }
 
 done:
@@ -1224,15 +1268,7 @@
     }
     free(args);
     buffer[size] = '\0';
-
-    char* line = strtok(buffer, "\n");
-    while (line) {
-        fprintf(((UpdaterInfo*)(state->cookie))->cmd_pipe,
-                "ui_print %s\n", line);
-        line = strtok(NULL, "\n");
-    }
-    fprintf(((UpdaterInfo*)(state->cookie))->cmd_pipe, "ui_print\n");
-
+    uiPrint(state, buffer);
     return StringValue(buffer);
 }
 
@@ -1504,6 +1540,37 @@
     return StringValue(strdup("t"));
 }
 
+Value* Tune2FsFn(const char* name, State* state, int argc, Expr* argv[]) {
+    if (argc == 0) {
+        return ErrorAbort(state, "%s() expects args, got %d", name, argc);
+    }
+
+    char** args = ReadVarArgs(state, argc, argv);
+    if (args == NULL) {
+        return ErrorAbort(state, "%s() could not read args", name);
+    }
+
+    int i;
+    char** args2 = malloc(sizeof(char*) * (argc+1));
+    // Tune2fs expects the program name as its args[0]
+    args2[0] = strdup(name);
+    for (i = 0; i < argc; ++i) {
+       args2[i + 1] = args[i];
+    }
+    int result = tune2fs_main(argc + 1, args2);
+    for (i = 0; i < argc; ++i) {
+        free(args[i]);
+    }
+    free(args);
+
+    free(args2[0]);
+    free(args2);
+    if (result != 0) {
+        return ErrorAbort(state, "%s() returned error code %d", name, result);
+    }
+    return StringValue(strdup("t"));
+}
+
 void RegisterInstallFunctions() {
     RegisterFunction("mount", MountFn);
     RegisterFunction("is_mounted", IsMountedFn);
@@ -1554,4 +1621,5 @@
     RegisterFunction("set_stage", SetStageFn);
 
     RegisterFunction("enable_reboot", EnableRebootFn);
+    RegisterFunction("tune2fs", Tune2FsFn);
 }