[ftl][aml-rawnand][fs-test] Reduce risk of accepting partial nand writes.

Pulls in changes to Ftl and raw nand driver to prevent interpretting
partial writes as valid pages. Includes test to reproduce and verify the
fix for the original issue.

Bug: b/179400686
(cherry picked from commits d2fdf2c7c0e3f4d8ab453ba5e3603b5d998cde9e
 249a0a1ffb19cf33f7e70be454db593ac3a91bd1
 c11a93bbfeb2f1d872a732920678aa24ec500eee
 dbdfdc370b2593c77df53264d2c976991e8e4203
 b1743da07826429c4a8495e4f8cad174903941b9)

Change-Id: I8ac619bab51863e0e5f7bdbe0fe6e64b1dc29bb0
diff --git a/sdk/fidl/fuchsia.hardware.nand/ram-nand.fidl b/sdk/fidl/fuchsia.hardware.nand/ram-nand.fidl
index 40ae8ab..fb8f667 100644
--- a/sdk/fidl/fuchsia.hardware.nand/ram-nand.fidl
+++ b/sdk/fidl/fuchsia.hardware.nand/ram-nand.fidl
@@ -64,6 +64,9 @@
     bool export_nand_config;
     /// if true, export a boot partition map as metadata.
     bool export_partition_map;
+
+    /// If non-zero, fail after `fail_after` writes.
+    uint32 fail_after;
 };
 
 [ForDeprecatedCBindings]
diff --git a/src/devices/block/drivers/ftl/nand_driver.cc b/src/devices/block/drivers/ftl/nand_driver.cc
index 084e731..f18296a 100644
--- a/src/devices/block/drivers/ftl/nand_driver.cc
+++ b/src/devices/block/drivers/ftl/nand_driver.cc
@@ -70,7 +70,16 @@
  public:
   NandDriverImpl(const nand_protocol_t* parent, const bad_block_protocol_t* bad_block,
                  ftl::OperationCounters* counters)
-      : parent_(parent), bad_block_protocol_(bad_block), counters_(counters) {}
+      : NandDriver(FtlLogger{
+            .trace = &LogTrace,
+            .debug = &LogDebug,
+            .info = &LogInfo,
+            .warn = &LogWarning,
+            .error = &LogError,
+        }),
+        parent_(parent),
+        bad_block_protocol_(bad_block),
+        counters_(counters) {}
   ~NandDriverImpl() final {}
 
   // NdmDriver interface:
@@ -84,6 +93,8 @@
   int IsBadBlock(uint32_t page_num) final;
   bool IsEmptyPage(uint32_t page_num, const uint8_t* data, const uint8_t* spare) final;
   void TryEraseRange(uint32_t start_block, uint32_t end_block) final;
+  uint32_t PageSize() final { return info_.page_size; }
+  uint8_t SpareSize() final { return info_.oob_size; }
   const nand_info_t& info() const final { return info_; }
 
  private:
@@ -137,14 +148,7 @@
   } else if (BadBbtReservation()) {
     return "Unable to use bad block reservation";
   }
-  ftl::LoggerProxy logger = {
-      .trace = &LogTrace,
-      .debug = &LogDebug,
-      .info = &LogInfo,
-      .warn = &LogWarning,
-      .error = &LogError,
-  };
-  const char* error = CreateNdmVolumeWithLogger(ftl_volume, options, true, logger);
+  const char* error = CreateNdmVolume(ftl_volume, options, true);
   if (error) {
     // Retry allowing the volume to be fixed as needed.
     zxlogf(INFO, "FTL: About to retry volume creation");
diff --git a/src/devices/block/drivers/ftl/nand_driver.h b/src/devices/block/drivers/ftl/nand_driver.h
index 322df16..71fa1c4 100644
--- a/src/devices/block/drivers/ftl/nand_driver.h
+++ b/src/devices/block/drivers/ftl/nand_driver.h
@@ -42,6 +42,9 @@
   // Cleans all non bad blocks in a given block range. Erase failures are logged amd deemed non
   // fatal.
   virtual void TryEraseRange(uint32_t start_block, uint32_t end_block) = 0;
+
+ protected:
+  NandDriver(FtlLogger logger) : NdmBaseDriver(logger) {}
 };
 
 }  // namespace ftl.
diff --git a/src/devices/block/drivers/ftl/tests/ndm-ram-driver.cc b/src/devices/block/drivers/ftl/tests/ndm-ram-driver.cc
index 90cd103..6a680af 100644
--- a/src/devices/block/drivers/ftl/tests/ndm-ram-driver.cc
+++ b/src/devices/block/drivers/ftl/tests/ndm-ram-driver.cc
@@ -248,6 +248,10 @@
   return ftl::kNdmOk;
 }
 
+uint32_t NdmRamDriver::PageSize() { return options_.page_size; }
+
+uint8_t NdmRamDriver::SpareSize() { return options_.eb_size; }
+
 bool NdmRamDriver::SimulateBadBlock(uint32_t page_num) {
   if (num_bad_blocks_ < options_.max_bad_blocks) {
     bad_block_interval_++;
diff --git a/src/devices/block/drivers/ftl/tests/ndm-ram-driver.h b/src/devices/block/drivers/ftl/tests/ndm-ram-driver.h
index 4d8b083..cb5dd14 100644
--- a/src/devices/block/drivers/ftl/tests/ndm-ram-driver.h
+++ b/src/devices/block/drivers/ftl/tests/ndm-ram-driver.h
@@ -47,7 +47,7 @@
  public:
   explicit NdmRamDriver(const ftl::VolumeOptions& options) : NdmRamDriver(options, {}) {}
   NdmRamDriver(const ftl::VolumeOptions& options, const TestOptions& test_options)
-      : options_(options), test_options_(test_options) {}
+      : NdmBaseDriver(ftl::DefaultLogger()), options_(options), test_options_(test_options) {}
   ~NdmRamDriver() final = default;
 
   // Extends the visible volume to the whole size of the storage.
@@ -75,6 +75,8 @@
   int NandErase(uint32_t page_num) final;
   int IsBadBlock(uint32_t page_num) final;
   bool IsEmptyPage(uint32_t page_num, const uint8_t* data, const uint8_t* spare) final;
+  uint32_t PageSize() final;
+  uint8_t SpareSize() final;
 
  private:
   // Reads or Writes a single page.
diff --git a/src/devices/nand/drivers/ram-nand/ram-nand.cc b/src/devices/nand/drivers/ram-nand/ram-nand.cc
index 53e610f..c27e6fe 100644
--- a/src/devices/nand/drivers/ram-nand/ram-nand.cc
+++ b/src/devices/nand/drivers/ram-nand/ram-nand.cc
@@ -5,6 +5,8 @@
 #include "ram-nand.h"
 
 #include <lib/ddk/binding.h>
+#include <lib/ddk/debug.h>
+#include <lib/ddk/metadata.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -15,7 +17,6 @@
 #include <algorithm>
 #include <utility>
 
-#include <lib/ddk/metadata.h>
 #include <ddk/metadata/bad-block.h>
 #include <ddk/metadata/nand.h>
 #include <fbl/algorithm.h>
@@ -141,6 +142,11 @@
       {BIND_NAND_CLASS, 0, params_.nand_class},
   };
 
+  fail_after_ = info.fail_after;
+  if (fail_after_ > 0) {
+    zxlogf(INFO, "fail-after: %u", fail_after_);
+  }
+
   return DdkAdd(ddk::DeviceAddArgs(name).set_props(props));
 }
 
@@ -323,12 +329,36 @@
     zx_status_t status = ZX_OK;
 
     switch (operation->command) {
-      case NAND_OP_READ:
       case NAND_OP_WRITE:
+        if (fail_after_ > 0) {
+          if (write_count_ >= fail_after_) {
+            status = ZX_ERR_IO;
+            break;
+          }
+          if (write_count_ + operation->rw.length > fail_after_) {
+            const uint32_t old_length = operation->rw.length;
+            operation->rw.length = fail_after_ - write_count_;
+            status = ReadWriteData(operation);
+            if (status == ZX_OK) {
+              status = ReadWriteOob(operation);
+            }
+            if (status == ZX_OK) {
+              write_count_ = fail_after_;
+              status = ZX_ERR_IO;
+            }
+            operation->rw.length = old_length;
+            break;
+          }
+        }
+        __FALLTHROUGH;
+      case NAND_OP_READ:
         status = ReadWriteData(operation);
         if (status == ZX_OK) {
           status = ReadWriteOob(operation);
         }
+        if (status == ZX_OK && operation->command == NAND_OP_WRITE) {
+          write_count_ += operation->rw.length;
+        }
         break;
 
       case NAND_OP_ERASE: {
diff --git a/src/devices/nand/drivers/ram-nand/ram-nand.h b/src/devices/nand/drivers/ram-nand/ram-nand.h
index 4f73814..57d05e2 100644
--- a/src/devices/nand/drivers/ram-nand/ram-nand.h
+++ b/src/devices/nand/drivers/ram-nand/ram-nand.h
@@ -112,6 +112,12 @@
   std::optional<nand_config_t> export_nand_config_;
   fbl::Array<char> export_partition_map_;
 
+  // If non-zero, the driver will fail writes once the write-count reaches this value.
+  uint32_t fail_after_ = 0;
+
+  // The number of pages written.
+  uint32_t write_count_ = 0;
+
   DISALLOW_COPY_ASSIGN_AND_MOVE(NandDevice);
 };
 
diff --git a/src/storage/blobfs/test/integration/sync_test.cc b/src/storage/blobfs/test/integration/sync_test.cc
index 7bdde1b..be30974 100644
--- a/src/storage/blobfs/test/integration/sync_test.cc
+++ b/src/storage/blobfs/test/integration/sync_test.cc
@@ -76,7 +76,7 @@
   ASSERT_EQ(vmo.CreateAndMap(kVmoSize, "vmo"), ZX_OK);
   memset(vmo.start(), 0xff, kVmoSize);
 
-  auto options = BlobfsDefaultTestParam();
+  auto options = BlobfsWithFvmTestParam();
   options.use_ram_nand = true;
   options.ram_nand_vmo = vmo.vmo().borrow();
   options.device_block_count = 0;  // Uses VMO size.
diff --git a/src/storage/fs_test/corrupt.cc b/src/storage/fs_test/corrupt.cc
index db14855..fcab3ac 100644
--- a/src/storage/fs_test/corrupt.cc
+++ b/src/storage/fs_test/corrupt.cc
@@ -5,6 +5,7 @@
 #include <fuchsia/device/llcpp/fidl.h>
 #include <lib/fdio/directory.h>
 #include <lib/fzl/owned-vmo-mapper.h>
+#include <lib/syslog/cpp/macros.h>
 #include <lib/zx/channel.h>
 
 #include <random>
@@ -24,27 +25,47 @@
   // 768 blocks containing 64 pages of 4 KiB with 8 bytes OOB
   constexpr int kSize = 768 * 64 * (4096 + 8);
 
-  fzl::OwnedVmoMapper vmo;
-  ASSERT_EQ(vmo.CreateAndMap(kSize, "corrupt-test-vmo"), ZX_OK);
-  memset(vmo.start(), 0xff, kSize);
+  for (int pass = 0; pass < 2; ++pass) {
+    fzl::OwnedVmoMapper vmo;
+    ASSERT_EQ(vmo.CreateAndMap(kSize, "corrupt-test-vmo"), ZX_OK);
+    memset(vmo.start(), 0xff, kSize);
 
-  TestFilesystemOptions options = TestFilesystemOptions::DefaultMinfs();
-  options.device_block_size = 8192;
-  options.device_block_count = 0;  // Use VMO size.
-  options.use_ram_nand = true;
-  options.ram_nand_vmo = zx::unowned_vmo(vmo.vmo());
+    TestFilesystemOptions options = TestFilesystemOptions::DefaultMinfs();
+    options.device_block_size = 8192;
+    options.device_block_count = 0;  // Use VMO size.
+    options.use_ram_nand = true;
+    options.ram_nand_vmo = zx::unowned_vmo(vmo.vmo());
+    std::random_device random;
 
-  // In one thread, repeatedly create a file, write to it, sync and then delete two files.  Then,
-  // some random amount of time later, tear down the Ram Nand driver, then rebind and fsck.  The
-  // journal should ensure the file system remains consistent.
-  {
-    auto fs_or = TestFilesystem::Create(options);
-    ASSERT_TRUE(fs_or.is_ok()) << fs_or.status_string();
-    TestFilesystem fs = std::move(fs_or).value();
+    if (pass == 0) {
+      // In this first pass, a write failure is closely targeted such that the failure is more
+      // likely to occur just as the FTL is writing the first page of a new map block.  If things
+      // change with the write pattern for minfs, then this range might not be right, so on the
+      // second pass, we target a much wider range.
+      std::uniform_int_distribution distribution(1325, 1400);
 
-    const std::string file1 = fs.mount_path() + "/file1";
-    const std::string file2 = fs.mount_path() + "/file2";
-    std::thread thread([&]() {
+      // Deliberately fail after an odd number so that we always fail half-way through an 8 KiB
+      // write.
+      options.fail_after = distribution(random) | 1;
+    } else {
+      // On the second pass, we use a wider random range in case a change in the system means that
+      // the first pass no longer targets weak spots.
+      std::uniform_int_distribution distribution(1300, 2300);
+      options.fail_after = distribution(random);
+    }
+
+    // Create a dummy FVM partition that shifts the location of the minfs partition such that the
+    // offsets being used will hit the second half of the FTL's 8 KiB map pages.
+    options.dummy_fvm_partition_size = 8'388'608;
+
+    {
+      auto fs_or = TestFilesystem::Create(options);
+      ASSERT_TRUE(fs_or.is_ok()) << fs_or.status_string();
+      TestFilesystem fs = std::move(fs_or).value();
+
+      // Loop until we encounter write failures.
+      const std::string file1 = fs.mount_path() + "/file1";
+      const std::string file2 = fs.mount_path() + "/file2";
       for (;;) {
         {
           fbl::unique_fd fd(open(file1.c_str(), O_RDWR | O_CREAT, 0644));
@@ -61,30 +82,16 @@
           }
         }
       }
-    });
+    }
 
-    std::random_device random;
-    std::uniform_int_distribution distribution(0, 20000);
-    const int usec = distribution(random);
-    std::cout << "sleeping for " << usec << "us" << std::endl;
-    zx_nanosleep(zx_deadline_after(zx::usec(usec).get()));
-
-    // Unbind the NAND driver.
-    zx::channel local, remote;
-    ASSERT_EQ(zx::channel::create(0, &local, &remote), ZX_OK);
-    ASSERT_EQ(fdio_service_connect(fs.GetRamNand()->path(), remote.release()), ZX_OK);
-    auto resp = fidl::WireCall<device::Controller>(local.borrow()).ScheduleUnbind();
-    ASSERT_EQ(resp.status(), ZX_OK);
-    ASSERT_FALSE(resp->result.is_err());
-
-    thread.join();
+    std::cout << "Remounting" << std::endl;
+    options.fail_after = 0;
+    auto fs_or = TestFilesystem::Open(options);
+    ASSERT_TRUE(fs_or.is_ok()) << fs_or.status_string();
+    TestFilesystem fs = std::move(fs_or).value();
+    EXPECT_EQ(fs.Unmount().status_value(), ZX_OK);
+    EXPECT_EQ(fs.Fsck().status_value(), ZX_OK);
   }
-
-  auto fs_or = TestFilesystem::Open(options);
-  ASSERT_TRUE(fs_or.is_ok()) << fs_or.status_string();
-  TestFilesystem fs = std::move(fs_or).value();
-  EXPECT_EQ(fs.Unmount().status_value(), ZX_OK);
-  EXPECT_EQ(fs.Fsck().status_value(), ZX_OK);
 }
 
 }  // namespace
diff --git a/src/storage/fs_test/fs_test.cc b/src/storage/fs_test/fs_test.cc
index 1ce866c..0a47c6f 100644
--- a/src/storage/fs_test/fs_test.cc
+++ b/src/storage/fs_test/fs_test.cc
@@ -29,6 +29,7 @@
 #include <fbl/unique_fd.h>
 #include <fs-management/admin.h>
 #include <fs-management/format.h>
+#include <fs-management/fvm.h>
 #include <fs-management/launch.h>
 #include <fs-management/mount.h>
 
@@ -79,20 +80,8 @@
     return ram_disk_or.take_error();
   }
 
-  // Create an FVM partition if requested.
-  std::string device_path;
-  if (options.use_fvm) {
-    auto fvm_partition_or =
-        storage::CreateFvmPartition(ram_disk_or.value().path(), options.fvm_slice_size);
-    if (fvm_partition_or.is_error()) {
-      return fvm_partition_or.take_error();
-    }
-    device_path = fvm_partition_or.value();
-  } else {
-    device_path = ram_disk_or.value().path();
-  }
-
-  return zx::ok(std::make_pair(std::move(ram_disk_or).value(), device_path));
+  std::string device_path = ram_disk_or.value().path();
+  return zx::ok(std::make_pair(std::move(ram_disk_or).value(), std::move(device_path)));
 }
 
 // Creates a ram-nand device.  It does not create an FVM partition; that is left to the caller.
@@ -152,7 +141,8 @@
       .nand_info.num_blocks = block_count,
       .nand_info.ecc_bits = 8,
       .nand_info.oob_size = kOobSize,
-      .nand_info.nand_class = fuchsia_hardware_nand_Class_FTL};
+      .nand_info.nand_class = fuchsia_hardware_nand_Class_FTL,
+      .fail_after = options.fail_after};
   status = zx::make_status(ramdevice_client::RamNand::Create(&config, &ram_nand));
   if (status.is_error()) {
     std::cout << "RamNand::Create failed: " << status.status_string() << std::endl;
@@ -208,28 +198,59 @@
 // Returns device and device path.
 zx::status<std::pair<RamDevice, std::string>> CreateRamDevice(
     const TestFilesystemOptions& options) {
+  RamDevice ram_device;
+  std::string device_path;
+
   if (options.use_ram_nand) {
     auto ram_nand_or = CreateRamNand(options);
     if (ram_nand_or.is_error()) {
       return ram_nand_or.take_error();
     }
     auto [ram_nand, nand_device_path] = std::move(ram_nand_or).value();
-
-    auto fvm_partition_or = storage::CreateFvmPartition(nand_device_path, options.fvm_slice_size);
-    if (fvm_partition_or.is_error()) {
-      std::cout << "Failed to create FVM partition: " << fvm_partition_or.status_string()
-                << std::endl;
-      return fvm_partition_or.take_error();
-    }
-
-    return zx::ok(std::make_pair(std::move(ram_nand), std::move(fvm_partition_or).value()));
+    ram_device = RamDevice(std::move(ram_nand));
+    device_path = std::move(nand_device_path);
   } else {
     auto ram_disk_or = CreateRamDisk(options);
     if (ram_disk_or.is_error()) {
       return ram_disk_or.take_error();
     }
-    auto [device, device_path] = std::move(ram_disk_or).value();
-    return zx::ok(std::make_pair(std::move(device), std::move(device_path)));
+    auto [device, ram_disk_path] = std::move(ram_disk_or).value();
+    ram_device = RamDevice(std::move(device));
+    device_path = std::move(ram_disk_path);
+  }
+
+  // Create an FVM partition if requested.
+  if (options.use_fvm) {
+    auto fvm_partition_or = storage::CreateFvmPartition(device_path, options.fvm_slice_size);
+    if (fvm_partition_or.is_error()) {
+      return fvm_partition_or.take_error();
+    }
+
+    if (options.dummy_fvm_partition_size > 0) {
+      auto fvm_fd = fbl::unique_fd(open((device_path + "/fvm").c_str(), O_RDWR));
+      if (!fvm_fd) {
+        std::cout << "Could not open FVM driver: " << strerror(errno) << std::endl;
+        return zx::error(ZX_ERR_BAD_STATE);
+      }
+
+      alloc_req_t request = {
+          .slice_count = options.dummy_fvm_partition_size / options.fvm_slice_size,
+          .type = {0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01,
+                   0x02, 0x03, 0x04},
+          .guid = {0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, 0x01,
+                   0x02, 0x03, 0x04},
+          .name = "dummy",
+      };
+      fbl::unique_fd fd(fvm_allocate_partition(fvm_fd.get(), &request));
+      if (!fd) {
+        std::cout << "Could not allocate dummy FVM partition" << std::endl;
+        return zx::error(ZX_ERR_BAD_STATE);
+      }
+    }
+
+    return zx::ok(std::make_pair(std::move(ram_device), std::move(fvm_partition_or).value()));
+  } else {
+    return zx::ok(std::make_pair(std::move(ram_device), std::move(device_path)));
   }
 }
 
diff --git a/src/storage/fs_test/fs_test.h b/src/storage/fs_test/fs_test.h
index 62bfd066b..03166eb 100644
--- a/src/storage/fs_test/fs_test.h
+++ b/src/storage/fs_test/fs_test.h
@@ -51,6 +51,11 @@
   // for OOB.
   zx::unowned_vmo ram_nand_vmo;
   bool use_fvm = false;
+
+  // If non-zero, create a dummy FVM partition which has the effect of moving the location of the
+  // partition under test to be at a different offset on the underlying device.
+  uint64_t dummy_fvm_partition_size = 0;
+
   uint64_t device_block_size = 0;
   uint64_t device_block_count = 0;
   uint64_t fvm_slice_size = 0;
@@ -62,6 +67,9 @@
 
   // The format blobfs should store blobs in.
   std::optional<blobfs::BlobLayoutFormat> blob_layout_format;
+
+  // If using ram_nand, the number of writes after which writes should fail.
+  uint32_t fail_after;
 };
 
 std::ostream& operator<<(std::ostream& out, const TestFilesystemOptions& options);
diff --git a/src/storage/testing/fvm.cc b/src/storage/testing/fvm.cc
index ba8307c..cb30a2c 100644
--- a/src/storage/testing/fvm.cc
+++ b/src/storage/testing/fvm.cc
@@ -79,20 +79,18 @@
     return zx::error(ZX_ERR_BAD_STATE);
   }
 
-  alloc_req_t request;
-  memset(&request, 0, sizeof(request));
-  request.slice_count = 1;
+  alloc_req_t request = {.slice_count = 1};
   memcpy(request.name, options.name.data(), options.name.size());
   request.name[options.name.size()] = 0;
   memcpy(request.type, options.type ? options.type->data() : kTestPartGUID, sizeof(request.type));
   memcpy(request.guid, kTestUniqueGUID, sizeof(request.guid));
 
-  fbl::unique_fd fd(fvm_allocate_partition(fvm_fd.get(), &request));
+  auto fd = fbl::unique_fd(fvm_allocate_partition(fvm_fd.get(), &request));
   if (!fd) {
     FX_LOGS(ERROR) << "Could not allocate FVM partition";
     return zx::error(ZX_ERR_BAD_STATE);
   }
-  close(fvm_fd.release());
+  fvm_fd.reset();
 
   char partition_path[PATH_MAX];
   fd.reset(open_partition(kTestUniqueGUID, request.type, 0, partition_path));
diff --git a/src/storage/volume_image/ftl/ftl_test_helper.h b/src/storage/volume_image/ftl/ftl_test_helper.h
index 56818e2..f725485 100644
--- a/src/storage/volume_image/ftl/ftl_test_helper.h
+++ b/src/storage/volume_image/ftl/ftl_test_helper.h
@@ -26,7 +26,10 @@
 class InMemoryNdm final : public ftl::NdmBaseDriver {
  public:
   explicit InMemoryNdm(InMemoryRawNand* raw_nand, uint64_t page_size, uint64_t oob_size)
-      : raw_nand_(raw_nand), page_size_(page_size), oob_size_(oob_size) {}
+      : NdmBaseDriver(ftl::DefaultLogger()),
+        raw_nand_(raw_nand),
+        page_size_(page_size),
+        oob_size_(oob_size) {}
 
   // Performs driver initialization. Returns an error string, or nullptr on
   // success.
@@ -65,6 +68,9 @@
   // the contents of the page.
   bool IsEmptyPage(uint32_t page_num, const uint8_t* data, const uint8_t* spare) final;
 
+  uint32_t PageSize() final { return static_cast<uint32_t>(page_size_); }
+  uint8_t SpareSize() final { return static_cast<uint8_t>(oob_size_); }
+
  private:
   InMemoryRawNand* raw_nand_ = nullptr;
   uint64_t page_size_ = 0;
diff --git a/zircon/system/ulib/ftl-mtd/include/lib/ftl-mtd/nand-volume-driver.h b/zircon/system/ulib/ftl-mtd/include/lib/ftl-mtd/nand-volume-driver.h
index d40fff3..850af0f 100644
--- a/zircon/system/ulib/ftl-mtd/include/lib/ftl-mtd/nand-volume-driver.h
+++ b/zircon/system/ulib/ftl-mtd/include/lib/ftl-mtd/nand-volume-driver.h
@@ -39,6 +39,8 @@
   int NandErase(uint32_t page_num);
   int IsBadBlock(uint32_t page_num);
   bool IsEmptyPage(uint32_t page_num, const uint8_t* page_buffer, const uint8_t* oob_buffer);
+  uint32_t PageSize() final;
+  uint8_t SpareSize() final;
 
   DISALLOW_COPY_ASSIGN_AND_MOVE(NandVolumeDriver);
 
@@ -54,8 +56,6 @@
   uint32_t GetByteOffsetForPage(uint32_t real_page);
 
   uint32_t ByteOffset();
-  uint32_t MappedPageSize();
-  uint32_t MappedOobSize();
 
   uint32_t block_offset_;
   uint32_t page_multiplier_;
diff --git a/zircon/system/ulib/ftl-mtd/nand-volume-driver.cc b/zircon/system/ulib/ftl-mtd/nand-volume-driver.cc
index f9caf6e..b1cfb4a 100644
--- a/zircon/system/ulib/ftl-mtd/nand-volume-driver.cc
+++ b/zircon/system/ulib/ftl-mtd/nand-volume-driver.cc
@@ -41,7 +41,8 @@
 NandVolumeDriver::NandVolumeDriver(uint32_t block_offset, uint32_t max_bad_blocks,
                                    uint32_t page_multiplier,
                                    std::unique_ptr<mtd::NandInterface> interface)
-    : block_offset_(block_offset),
+    : NdmBaseDriver(ftl::DefaultLogger()),
+      block_offset_(block_offset),
       page_multiplier_(page_multiplier),
       max_bad_blocks_(max_bad_blocks),
       interface_(std::move(interface)) {}
@@ -54,8 +55,8 @@
       // This should be 2%, but that is of the whole device, not just this partition.
       .max_bad_blocks = max_bad_blocks_,
       .block_size = interface_->BlockSize(),
-      .page_size = MappedPageSize(),
-      .eb_size = MappedOobSize(),
+      .page_size = PageSize(),
+      .eb_size = SpareSize(),
       .flags = 0  // Same as FSF_DRVR_PAGES (current default).
   };
 
@@ -155,7 +156,7 @@
 
 bool NandVolumeDriver::IsEmptyPage(uint32_t page_num, const uint8_t* page_buffer,
                                    const uint8_t* oob_buffer) {
-  return IsEmptyPageImpl(page_buffer, MappedPageSize(), oob_buffer, MappedOobSize());
+  return IsEmptyPageImpl(page_buffer, PageSize(), oob_buffer, SpareSize());
 }
 
 zx_status_t NandVolumeDriver::ReadPageAndOob(uint32_t byte_offset, void* page_buffer,
@@ -208,8 +209,8 @@
 
 uint32_t NandVolumeDriver::ByteOffset() { return block_offset_ * interface_->BlockSize(); }
 
-uint32_t NandVolumeDriver::MappedPageSize() { return page_multiplier_ * interface_->PageSize(); }
+uint32_t NandVolumeDriver::PageSize() { return page_multiplier_ * interface_->PageSize(); }
 
-uint32_t NandVolumeDriver::MappedOobSize() { return page_multiplier_ * interface_->OobSize(); }
+uint8_t NandVolumeDriver::SpareSize() { return page_multiplier_ * interface_->OobSize(); }
 
 }  // namespace ftl_mtd
diff --git a/zircon/system/ulib/ftl-mtd/test/ftl-volume-wrapper-tests.cc b/zircon/system/ulib/ftl-mtd/test/ftl-volume-wrapper-tests.cc
index 26cf298..779a7393 100644
--- a/zircon/system/ulib/ftl-mtd/test/ftl-volume-wrapper-tests.cc
+++ b/zircon/system/ulib/ftl-mtd/test/ftl-volume-wrapper-tests.cc
@@ -99,7 +99,8 @@
 
     ftl_volume_wrapper_ = std::make_unique<FtlVolumeWrapper>(std::move(volume));
     volume_->set_ftl_instance(ftl_volume_wrapper_.get());
-    ASSERT_OK(ftl_volume_wrapper_->Init(std::unique_ptr<ftl::NdmBaseDriver>()));
+    ASSERT_OK(
+        ftl_volume_wrapper_->Init(std::unique_ptr<ftl::NdmBaseDriver>(ftl::DefaultLogger())));
     ASSERT_TRUE(volume_->initialized());
   }
 
diff --git a/zircon/system/ulib/ftl/BUILD.gn b/zircon/system/ulib/ftl/BUILD.gn
index 2886026..60e88424b 100644
--- a/zircon/system/ulib/ftl/BUILD.gn
+++ b/zircon/system/ulib/ftl/BUILD.gn
@@ -37,6 +37,7 @@
     "ftln/ftln_intrnl.c",
     "ftln/ftln_rd.c",
     "ftln/ftln_util.c",
+    "ftln/stats.c",
     "ftln/ndm-driver.cc",
     "ftln/volume.cc",
     "ndm/ndm_init.c",
diff --git a/zircon/system/ulib/ftl/ftl.h b/zircon/system/ulib/ftl/ftl.h
index 7741d27..132f03b 100644
--- a/zircon/system/ulib/ftl/ftl.h
+++ b/zircon/system/ulib/ftl/ftl.h
@@ -5,6 +5,7 @@
 #ifndef ZIRCON_SYSTEM_ULIB_FTL_FTL_H_
 #define ZIRCON_SYSTEM_ULIB_FTL_FTL_H_
 
+#include <lib/ftl/logger.h>
 #include <stdint.h>
 #include <zircon/compiler.h>
 
@@ -189,18 +190,6 @@
 typedef struct ndm* NDM;
 typedef const struct ndm* CNDM;
 
-typedef void (*LogFunction)(const char*, int, const char*, ...) __PRINTFLIKE(3, 4);
-
-typedef struct {
-  // Logger interface for different log levels.
-  LogFunction trace;
-  LogFunction debug;
-  LogFunction info;
-  LogFunction warning;
-  LogFunction error;
-  LogFunction fatal;
-} Logger;
-
 // FTL NDM structure holding all driver information.
 typedef struct {
   uint32_t block_size;        // Size of a block in bytes.
@@ -213,7 +202,7 @@
   uint32_t read_wear_limit;   // Device read-wear limit.
   void* ndm;                  // Driver's NDM pointer.
   uint32_t flags;             // Option flags.
-  Logger logger;
+  FtlLogger logger;
 } FtlNdmVol;
 
 // TargetNDM Configuration Structure.
@@ -252,7 +241,7 @@
   int (*rd_raw_spare)(uint32_t p, uint8_t* spare, void* dev);
   int (*rd_raw_page)(uint32_t p, void* data, void* dev);
 #endif
-  Logger logger;
+  FtlLogger logger;
 } NDMDrvr;
 
 // Driver count statistics for TargetFTL-NDM volumes.
diff --git a/zircon/system/ulib/ftl/ftln/ftln_init.c b/zircon/system/ulib/ftl/ftln/ftln_init.c
index 650c58f..680ccb9 100644
--- a/zircon/system/ulib/ftl/ftln/ftln_init.c
+++ b/zircon/system/ulib/ftl/ftln/ftln_init.c
@@ -203,8 +203,8 @@
 
             // If page is erased or invalid, return its status.
             if (status != NDM_PAGE_VALID) {
-              ftl->logger.warning(__FILE__, __LINE__,
-                                  "Erased or Invalid page found in erase block list.");
+              ftl->logger.warn(__FILE__, __LINE__,
+                               "Erased or Invalid page found in erase block list.");
               return status;
             }
 
@@ -345,10 +345,10 @@
       if (po == 0) {
         bc = GET_SA_BC(ftl->spare_buf);
       } else if (bc != GET_SA_BC(ftl->spare_buf)) {
-#if FTLN_DEBUG > 1
-        ftl->logger.debug(__FILE__, __LINE__, "build_map: b = %u, po = %u, i_bc = %u vs 0_bc = %u",
-                          b, po, GET_SA_BC(ftl->spare_buf), bc);
-#endif
+        if (ftln_debug() > 1) {
+          ftl->logger.debug(__FILE__, __LINE__, "build_map: b = %u, po = %u, i_bc = %u vs 0_bc = %u",
+                            b, po, GET_SA_BC(ftl->spare_buf), bc);
+        }
 
         // Should not be, but page is invalid. Break to skip block.
         break;
@@ -364,10 +364,10 @@
       // Retrieve MPN and check that it is valid.
       mpn = GET_SA_VPN(ftl->spare_buf);
       if (mpn > ftl->num_map_pgs) {
-#if FTLN_DEBUG > 1
-        ftl->logger.debug(__FILE__, __LINE__, "build_map: b = %u, po = %u, mpn = %u, max = %u", b,
-                          po, mpn, ftl->num_map_pgs);
-#endif
+        if (ftln_debug() > 1) {
+          ftl->logger.debug(__FILE__, __LINE__, "build_map: b = %u, po = %u, mpn = %u, max = %u", b,
+                            po, mpn, ftl->num_map_pgs);
+        }
 
         // Should not be, but page is invalid. Break to skip block.
         break;
@@ -392,10 +392,10 @@
           PfAssert(IS_MAP_BLK(ftl->bdata[b]));
           INC_USED(ftl->bdata[b]);
         }
-#if FTLN_DEBUG > 1
-        ftl->logger.debug(__FILE__, __LINE__, "build_map: mpn = %u, old_pn = %d, new_pn = %u", mpn,
-                          ftl->mpns[mpn], b * ftl->pgs_per_blk + po);
-#endif
+        if (ftln_debug() > 1) {
+          ftl->logger.debug(__FILE__, __LINE__, "build_map: mpn = %u, old_pn = %d, new_pn = %u", mpn,
+                            ftl->mpns[mpn], b * ftl->pgs_per_blk + po);
+        }
 
         // Save the map page number and (temporarily) the block count.
         ftl->mpns[mpn] = b * ftl->pgs_per_blk + po;
@@ -424,9 +424,9 @@
     if (pn == (ui32)-1)
       continue;
 
-#if FTLN_DEBUG > 1
-    ftl->logger.debug(__FILE__, __LINE__, "  -> MPN[%2u] = %u", mpn, pn);
-#endif
+    if (ftln_debug() > 1) {
+      ftl->logger.debug(__FILE__, __LINE__, "  -> MPN[%2u] = %u", mpn, pn);
+    }
 
     // Read map page. Return -1 if error.
     if (FtlnRdPage(ftl, pn, ftl->main_buf))
@@ -490,10 +490,10 @@
       }
     }
   }
-#if FTLN_DEBUG > 1
-  ftl->logger.debug(__FILE__, __LINE__, "volume block %d has lowest used page offset (%d)\n",
-                    ftl->resume_vblk, ftl->resume_po);
-#endif
+  if (ftln_debug() > 1) {
+    ftl->logger.debug(__FILE__, __LINE__, "volume block %d has lowest used page offset (%d)\n",
+                      ftl->resume_vblk, ftl->resume_po);
+  }
 
   // Clean temporary use of vol block read-wear field for page offset.
   for (b = 0; b < ftl->num_blks; ++b)
@@ -844,7 +844,7 @@
     uint32_t column = i % 8;
     sprintf(line_buffer + 5 * column, "%5u", wear_lag_histogram[255 - i]);
     if (column == 7) {
-      ftl->logger.info(__FILE__, __LINE__, line_buffer);
+      ftl->logger.info(__FILE__, __LINE__, "%s", line_buffer);
     }
   }
 
@@ -1203,12 +1203,12 @@
     }
   }
 
-#if FTLN_DEBUG > 1
-  ftl->logger.debug(
-      __FILE__, __LINE__,
-      "FTL formatted successfully. [Current Generation Number = %u  Highest Wear Count = %u",
-      ftl->high_bc, ftl->high_wc);
-#endif
+  if (ftln_debug() > 1) {
+    ftl->logger.debug(
+        __FILE__, __LINE__,
+        "FTL formatted successfully. [Current Generation Number = %u  Highest Wear Count = %u",
+        ftl->high_bc, ftl->high_wc);
+  }
 
   // Do recycles if needed and return status.
   return FtlnRecCheck(ftl, 0);
@@ -1221,10 +1221,10 @@
 static void free_ftl(void* vol) {
   FTLN ftl = vol;
 
-#if FTLN_DEBUG > 1
-  // Display FTL statistics.
-  FtlnStats(ftl);
-#endif
+  if (ftln_debug() > 1) {
+    // Display FTL statistics.
+    FtlnStats(ftl);
+  }
 
   // Free FTL memory allocations.
   if (ftl->bdata)
@@ -1452,10 +1452,10 @@
   ftl->num_map_pgs = 1 + (ftl->num_vpages + ftl->mappings_per_mpg - 1) / ftl->mappings_per_mpg;
   PfAssert(ftl->num_vpages / ftl->mappings_per_mpg < ftl->num_map_pgs);
 
-#if FTLN_DEBUG > 1
-  ftl->logger.debug(__FILE__, __LINE__, "Volume Pages = %u. FTL pages = %u. %u%% usage",
-                    ftl->num_vpages, ftl->num_pages, (ftl->num_vpages * 100) / ftl->num_pages);
-#endif
+  if (ftln_debug() > 1) {
+    ftl->logger.debug(__FILE__, __LINE__, "Volume Pages = %u. FTL pages = %u. %u%% usage",
+                      ftl->num_vpages, ftl->num_pages, (ftl->num_vpages * 100) / ftl->num_pages);
+  }
 
   // Allocate one or two main data pages and spare buffers. Max spare
   // use is one block worth of spare areas for multi-page writes.
@@ -1554,10 +1554,10 @@
   ftl->wear_data.lft_max_lag = ftl->wear_data.cur_max_lag;
   ftl->wear_data.avg_wc_lag = ftl->wc_lag_sum / ftl->num_blks;
 
-#if FTLN_DEBUG > 1
-  // Display FTL statistics.
-  FtlnStats(ftl);
-#endif
+  if (ftln_debug() > 1) {
+    // Display FTL statistics.
+    FtlnStats(ftl);
+  }
 
   // Initialize FTL interface structure.
   xfs->num_pages = ftl->num_vpages;
@@ -1567,9 +1567,9 @@
   xfs->report = FtlnReport;
   xfs->vol = ftl;
 
-#if FTLN_DEBUG > 1
-  ftl->logger.debug(__FILE__, __LINE__, "[XFS] num_pages       = %u\n\n", xfs->num_pages);
-#endif
+  if (ftln_debug() > 1) {
+    ftl->logger.debug(__FILE__, __LINE__, "[XFS] num_pages       = %u\n\n", xfs->num_pages);
+  }
 
   // Register FTL volume with user.
   if (XfsAddVol(xfs))
diff --git a/zircon/system/ulib/ftl/ftln/ftln_intrnl.c b/zircon/system/ulib/ftl/ftln/ftln_intrnl.c
index 8d6646d..c636cc9 100644
--- a/zircon/system/ulib/ftl/ftln/ftln_intrnl.c
+++ b/zircon/system/ulib/ftl/ftln/ftln_intrnl.c
@@ -583,10 +583,9 @@
 static int recycle_vblk(FTLN ftl, ui32 recycle_b) {
   ui32 pn, past_end;
 
-#if FTLN_DEBUG > 1
-  if (ftl->flags & FTLN_VERBOSE)
+  if (ftln_debug() > 1 && ftl->flags & FTLN_VERBOSE) {
     printf("recycle_vblk: block# %u\n", recycle_b);
-#endif
+  }
 
   // Transfer all used pages from recycle block to free block.
   pn = recycle_b * ftl->pgs_per_blk;
@@ -924,10 +923,9 @@
 int FtlnRecycleMapBlk(FTLN ftl, ui32 recycle_b) {
   ui32 i, pn;
 
-#if FTLN_DEBUG > 1
-  if (ftl->flags & FTLN_VERBOSE)
+  if (ftln_debug() > 1 && ftl->flags & FTLN_VERBOSE) {
     printf("FtlnRecycleMapBlk: block# %u\n", recycle_b);
-#endif
+  }
 
   // Transfer all used pages from recycle block to free block.
   pn = recycle_b * ftl->pgs_per_blk;
@@ -984,6 +982,104 @@
   return 0;
 }
 
+//  FtlnCalculateSpareValidity: Returns a validity byte for the spare using
+//                              a bitwise check where each bit covers 2
+//                              bytes of the spare and 1/8th of the page data.
+//                              1 if all bytes are 0xffff and 0 otherwise.
+//                              When performing this, we ensure that the byte
+//                              this will write out to is read as 0, ensuring a
+//                              non-0xff result.
+//
+//              Inputs: spare_buf = The pointer to the spare data to be read and
+//                                  validated.
+//
+//                       data_buf = The pointer to the page data to be
+//                                  validated.
+//
+//                      page_size = The size of the data page in bytes.
+//
+//              Returns: The calculated validity byte.
+ui8 FtlnCalculateSpareValidity(const ui8* spare_buf, const ui8* data_buf, ui32 page_size) {
+  ui16* oob_to_check = (ui16*)spare_buf;
+  ui8 result = 0;
+  const ui32 page_chunk = page_size / 8;
+
+  // Only check 7/8 byte pairs and shift at the end. Since we interpret
+  // where this check byte goes as zero, so it will always be a zero.
+  for (ui32 i = 0; i < 7; i++) {
+    if (oob_to_check[i] == 0xffff) {
+      // Breaking 32 bit constraint here, because fuchsia expects at least 64
+      // bit support anyways. Allows us to move at 8 byte strides.
+      uint64_t* data_to_check = (uint64_t*)&data_buf[page_chunk * i];
+      for (ui32 j = 0; j < page_chunk / 8; j++) {
+        if (data_to_check[j] != 0xffffffffffffffff) {
+          break;
+        }
+      }
+      result |= 1;
+    }
+    result <<= 1;
+  }
+  return result;
+}
+
+//  FtlnSetSpareValidity: Sets the last validity byte of the spare to the
+//                        result of FtlnCalculateSpareValidity.
+//
+//              Inputs: spare_buf = The pointer to the spare data to be read and
+//                                  updated.
+//
+//                       data_buf = The pointer to the page data to be
+//                                  validated.
+//
+//                      page_size = The size of the data page in bytes.
+//
+void FtlnSetSpareValidity(ui8* spare_buf, const ui8* data_buf, ui32 page_size) {
+  spare_buf[14] = FtlnCalculateSpareValidity(spare_buf, data_buf, page_size);
+}
+
+//  FtlnSetSpareValidity: Verifies that the last validity byte of the spare is
+//                        the result of FtlnCalculateSpareValidity.
+//
+//              Inputs: spare_buf = The pointer to the spare data to be read and
+//                                  validated.
+//
+//                       data_buf = The pointer to the page data to be
+//                                  validated.
+//
+//                      page_size = The size of the data page in bytes.
+//
+//              Returns: 1 if true, and 0 if false.
+//
+int FtlnCheckSpareValidity(const ui8* spare_buf, const ui8* data_buf, ui32 page_size) {
+  return spare_buf[14] == FtlnCalculateSpareValidity(spare_buf, data_buf, page_size);
+}
+
+//  FtlnIncompleteWrite: Verifies a complete write by checking the validity
+//                       byte if populated, otherwise checks that wear count
+//                       isn't maxed out. The wear count is used as a less
+//                       reliable, but reverse-compatible check for
+//                       incomplete oob data for before the validity byte was
+//                       used.
+//
+//              Inputs: spare_buf = The pointer to the spare data to be read and
+//                                  validated.
+//
+//                       data_buf = The pointer to the page data to be
+//                                  validated.
+//
+//                      page_size = The size of the data page in bytes.
+//
+//              Returns: 1 if incomplete write detected, and 0 if otherwise.
+//
+int FtlnIncompleteWrite(const ui8* spare_buf, const ui8* data_buf, ui32 page_size) {
+  if (spare_buf[14] != 0xff) {
+    return !FtlnCheckSpareValidity(spare_buf, data_buf, page_size);
+  } else {
+    return GET_SA_WC(spare_buf) == 0xfffffff ? 1 : 0;
+  }
+}
+
 //  FtlnMetaWr: Write FTL meta information page
 //
 //      Inputs: ftl = pointer to FTL control block
@@ -1032,14 +1128,13 @@
     // Count number of times any recycle is done in FtlnRecCheck().
     ++ftl->recycle_needed;
 
-#if FTLN_DEBUG > 1
-    if (ftl->flags & FTLN_VERBOSE)
+    if (ftln_debug() > 1 && ftl->flags & FTLN_VERBOSE) {
       printf(
           "\n0 rec begin: free vpn = %5u (%3u), free mpn = %5u (%3u)"
           " free blocks = %2u\n",
           ftl->free_vpn, free_vol_list_pgs(ftl), ftl->free_mpn, free_map_list_pgs(ftl),
           ftl->num_free_blks);
-#endif
+    }
 
     // Loop until enough pages are free.
     for (count = 1;; ++count) {
@@ -1055,20 +1150,19 @@
       // Record the highest number of consecutive recycles.
       if (ftl->wear_data.max_consec_rec < count) {
         ftl->wear_data.max_consec_rec = count;
-#if FTLN_DEBUG > 2
-        printf("max_consec_rec=%u, avg_wc_lag=%u\n", ftl->wear_data.max_consec_rec,
-               ftl->wear_data.avg_wc_lag);
-#endif
+        if (ftln_debug() > 2) {
+          printf("max_consec_rec=%u, avg_wc_lag=%u\n", ftl->wear_data.max_consec_rec,
+                 ftl->wear_data.avg_wc_lag);
+        }
       }
 
-#if FTLN_DEBUG > 1
-      if (ftl->flags & FTLN_VERBOSE)
+      if (ftln_debug() > 1 && ftl->flags & FTLN_VERBOSE) {
         printf(
             "%u rec begin: free vpn = %5u (%3u), free mpn = %5u (%3u)"
             " free blocks = %2u\n",
             count, ftl->free_vpn, free_vol_list_pgs(ftl), ftl->free_mpn, free_map_list_pgs(ftl),
             ftl->num_free_blks);
-#endif
+      }
 
       // Break if enough pages have been freed.
       if (FtlnRecNeeded(ftl, wr_cnt) == FALSE)
@@ -1085,10 +1179,10 @@
       // Ensure we haven't recycled too many times.
       PfAssert(count <= 2 * ftl->num_blks);
       if (count > 2 * ftl->num_blks) {
-#if FTLN_DEBUG
-        printf("FTL NDM too many consec recycles = %u\n", count);
-        FtlnBlkStats(ftl);
-#endif
+        if (ftln_debug() > 0) {
+          printf("FTL NDM too many consec recycles = %u\n", count);
+          FtlnBlkStats(ftl);
+        }
         return FsError2(FTL_RECYCLE_CNT, ENOSPC);
       }
     }
diff --git a/zircon/system/ulib/ftl/ftln/ftln_rd.c b/zircon/system/ulib/ftl/ftln/ftln_rd.c
index a2434f9..61fbc8e 100644
--- a/zircon/system/ulib/ftl/ftln/ftln_rd.c
+++ b/zircon/system/ulib/ftl/ftln/ftln_rd.c
@@ -73,7 +73,8 @@
 
   // Ensure request is within volume's range of provided pages.
   if (vpn + count > ftl->num_vpages) {
-    ftl->logger.error(__FILE__, __LINE__, "FTL Read failed. Attempting to read page %u is out of range(max %u).",
+    ftl->logger.error(__FILE__, __LINE__,
+                      "FTL Read failed. Attempting to read page %u is out of range(max %u).",
                       vpn + count - 1, ftl->num_pages - 1);
     return FsError2(FTL_ASSERT, ENOSPC);
   }
@@ -85,7 +86,7 @@
   // If there's at least a block with a maximum read count, recycle.
   if (ftl->max_rc_blk != (ui32)-1)
     if (FtlnRecCheck(ftl, 0)) {
-      ftl->logger.error(__FILE__, __LINE__, "FTL read recycle failed for page %u.");
+      ftl->logger.error(__FILE__, __LINE__, "FTL read recycle failed");
       return -1;
     }
 
diff --git a/zircon/system/ulib/ftl/ftln/ftln_util.c b/zircon/system/ulib/ftl/ftln/ftln_util.c
index 53324b4..f5cc8af 100644
--- a/zircon/system/ulib/ftl/ftln/ftln_util.c
+++ b/zircon/system/ulib/ftl/ftln/ftln_util.c
@@ -263,11 +263,11 @@
       }
 #endif  // INC_ELIST
 
-#if FTLN_DEBUG > 1
-      // Display FTL statistics.
-      FtlnStats(ftl);
-      FtlnBlkStats(ftl);
-#endif
+      if (ftln_debug() > 1) {
+        // Display FTL statistics.
+        FtlnStats(ftl);
+        FtlnBlkStats(ftl);
+      }
 
       // Return success.
       return 0;
@@ -372,14 +372,14 @@
       ftl->stats.ram_used = sizeof(struct ftln) + ftl->num_map_pgs * sizeof(ui32) + ftl->page_size +
                             ftl->eb_size * ftl->pgs_per_blk + ftlmcRAM(ftl->map_cache) +
                             ftl->num_blks * (sizeof(ui32) + sizeof(ui8));
-#if FTLN_DEBUG > 1
-      printf("TargetFTL-NDM RAM usage:\n");
-      printf(" - sizeof(Ftln) : %u\n", (int)sizeof(FTLN));
-      printf(" - tmp buffers  : %u\n", ftl->page_size + ftl->eb_size * ftl->pgs_per_blk);
-      printf(" - map pages    : %u\n", ftl->num_map_pgs * 4);
-      printf(" - map cache    : %u\n", ftlmcRAM(ftl->map_cache));
-      printf(" - bdata[]      : %u\n", ftl->num_blks * (int)(sizeof(ui32) + sizeof(ui8)));
-#endif
+      if (ftln_debug() > 1) {
+        printf("TargetFTL-NDM RAM usage:\n");
+        printf(" - sizeof(Ftln) : %u\n", (int)sizeof(FTLN));
+        printf(" - tmp buffers  : %u\n", ftl->page_size + ftl->eb_size * ftl->pgs_per_blk);
+        printf(" - map pages    : %u\n", ftl->num_map_pgs * 4);
+        printf(" - map cache    : %u\n", ftlmcRAM(ftl->map_cache));
+        printf(" - bdata[]      : %u\n", ftl->num_blks * (int)(sizeof(ui32) + sizeof(ui8)));
+      }
 
       const int kNumBuckets = sizeof(buf->wear_histogram) / sizeof(ui32);
       PfAssert(kNumBuckets == 20);
@@ -412,13 +412,13 @@
         return FsError2(FTL_MOUNTED, EEXIST);
       ftl->flags |= FTLN_MOUNTED;
 
-#if FTLN_DEBUG > 1
-      // Display FTL statistics.
-      FtlnStats(ftl);
-      FtlnBlkStats(ftl);
-#elif FTLN_DEBUG
-      printf("FTL: total blocks: %u, free blocks: %u\n", ftl->num_blks, ftl->num_free_blks);
-#endif
+      if (ftln_debug() > 1) {
+        // Display FTL statistics.
+        FtlnStats(ftl);
+        FtlnBlkStats(ftl);
+      } else {
+        printf("FTL: total blocks: %u, free blocks: %u\n", ftl->num_blks, ftl->num_free_blks);
+      }
 
       // Return success.
       return 0;
@@ -440,9 +440,9 @@
   if (ftl->free_vpn != (ui32)-1) {
     ui32 pn = ndmPastPrevPair(ftl->ndm, ftl->free_vpn);
 
-#if FTLN_DEBUG
-    printf("FtlnMlcSafeFreeVpn: old free = %u, new free = %u\n", ftl->free_vpn, pn);
-#endif
+    if (ftln_debug() > 0) {
+      printf("FtlnMlcSafeFreeVpn: old free = %u, new free = %u\n", ftl->free_vpn, pn);
+    }
     ftl->free_vpn = pn;
   }
 }
@@ -654,10 +654,10 @@
   FtlnStateRst(ftl);
   ftl->high_bc = 1;  // initial block count of unformatted volumes
 
-#if FTLN_DEBUG
-  // Display FTL statistics.
-  FtlnBlkStats(ftl);
-#endif
+  if (ftln_debug() > 0) {
+    // Display FTL statistics.
+    FtlnBlkStats(ftl);
+  }
 
   // Return success.
   return 0;
@@ -703,14 +703,14 @@
   PfAssert(!IS_FREE(ftl->bdata[b]));
   DEC_USED(ftl->bdata[b]);
 
-#if FTLN_DEBUG
-  // Read page spare area and assert VPNs match.
-  ++ftl->stats.read_spare;
-  // Ignore errors here.
-  if (ndmReadSpare(ftl->start_pn + pn, ftl->spare_buf, ftl->ndm) >= 0) {
-    PfAssert(GET_SA_VPN(ftl->spare_buf) == vpn);
+  if (ftln_debug() > 0) {
+    // Read page spare area and assert VPNs match.
+    ++ftl->stats.read_spare;
+    // Ignore errors here.
+    if (ndmReadSpare(ftl->start_pn + pn, ftl->spare_buf, ftl->ndm) >= 0) {
+      PfAssert(GET_SA_VPN(ftl->spare_buf) == vpn);
+    }
   }
-#endif
 }  // lint !e818
 
 //  FtlnFatErr: Process FTL-NDM fatal error
@@ -805,107 +805,6 @@
   return wear_data;
 }
 
-#if FTLN_DEBUG
-// flush_bstat: Flush buffered statistics counts
-//
-//      Inputs: ftl = pointer to FTL control block
-//              b = block number of current block
-//              type = "FREE", "MAP", or "VOLUME"
-//  In/Outputs: *blk0 = first consecutive block number or -1
-//              *blke = end consecutive block number
-//
-static void flush_bstat(CFTLN ftl, int* blk0, int* blke, int b, const char* type) {
-  if (*blk0 == -1)
-    *blk0 = *blke = b;
-  else if (*blke + 1 == b)
-    *blke = b;
-  else {
-    printf("B = %4u", *blk0);
-    if (*blk0 == *blke) {
-      printf(" - used = %2u, wc lag = %3d, rc = %8u", NUM_USED(ftl->bdata[*blk0]),
-             ftl->blk_wc_lag[*blk0], GET_RC(ftl->bdata[*blk0]));
-      printf(" - %s BLOCK\n", type);
-    } else {
-      printf("-%-4u", *blke);
-      printf("%*s", 37, " ");
-      printf("- %s BLOCKS\n", type);
-    }
-    *blk0 = *blke = b;
-  }
-}
-
-// FtlnBlkStats: Debug function to display blocks statistics
-//
-//       Input: ftl = pointer to FTL control block
-//
-void FtlnBlkStats(CFTLN ftl) {
-  int free0 = -1, freee, vol0 = -1, vole;
-  ui32 b;
-
-  printf(
-      "\nBLOCK STATS: %u blocks, %u pages per block, curr free "
-      "blocks = %u\n",
-      ftl->num_blks, ftl->pgs_per_blk, ftl->num_free_blks);
-
-  // Loop over FTL blocks.
-  for (b = 0; b < ftl->num_blks; ++b) {
-    // Check if block is free.
-    if (IS_FREE(ftl->bdata[b])) {
-      flush_bstat(ftl, &vol0, &vole, -1, "VOLUME");
-      flush_bstat(ftl, &free0, &freee, b, "FREE");
-    }
-
-    // Else check if map block.
-    else if (IS_MAP_BLK(ftl->bdata[b])) {
-      flush_bstat(ftl, &free0, &freee, -1, "FREE");
-      flush_bstat(ftl, &vol0, &vole, -1, "VOLUME");
-      printf("B = %4u - used = %2u, wc lag = %3d, rc = %8u - ", b, NUM_USED(ftl->bdata[b]),
-             ftl->blk_wc_lag[b], GET_RC(ftl->bdata[b]));
-      printf("MAP BLOCK\n");
-    }
-
-    // Else is volume block.
-    else {
-      flush_bstat(ftl, &free0, &freee, -1, "FREE");
-#if FTLN_DEBUG <= 1
-      flush_bstat(ftl, &vol0, &vole, b, "VOLUME");
-#else
-      printf("B = %4u - used = %2u, wc lag = %3d, rc = %8u - ", b, NUM_USED(ftl->bdata[b]),
-             ftl->blk_wc_lag[b], GET_RC(ftl->bdata[b]));
-      printf("VOLUME BLOCK\n");
-#endif
-    }
-  }
-  flush_bstat(ftl, &free0, &freee, -1, "FREE");
-  flush_bstat(ftl, &vol0, &vole, -1, "VOLUME");
-}
-#endif  // FTLN_DEBUG
-
-#if FTLN_DEBUG > 1
-//   FtlnStats: Display FTL statistics
-//
-//       Input: ftl = pointer to FTL control block
-//
-void FtlnStats(FTLN ftl) {
-  ui32 b, n;
-
-  printf("\nFTL STATS:\n");
-  printf("  - # vol pages    = %d\n", ftl->num_vpages);
-  printf("  - # map pages    = %d\n", ftl->num_map_pgs);
-  printf("  - # free blocks  = %d\n", ftl->num_free_blks);
-  for (n = b = 0; b < ftl->num_blks; ++b)
-    if (IS_ERASED(ftl->bdata[b]))
-      ++n;
-  printf("  - # erased blks  = %d\n", n);
-  printf("  - flags =");
-  if (ftl->flags & FTLN_FATAL_ERR)
-    printf(" FTLN_FATAL_ERR");
-  if (ftl->flags & FTLN_MOUNTED)
-    printf(" FTLN_MOUNTED");
-  putchar('\n');
-}
-#endif  // FTLN_DEBUG
-
 #if DEBUG_ELIST
 // FtlnCheckBlank: Ensure the specified block is blank
 //
diff --git a/zircon/system/ulib/ftl/ftln/ftlnp.h b/zircon/system/ulib/ftl/ftln/ftlnp.h
index b759c6a..7ad5554 100644
--- a/zircon/system/ulib/ftl/ftln/ftlnp.h
+++ b/zircon/system/ulib/ftl/ftln/ftlnp.h
@@ -28,6 +28,13 @@
 #define FTLN_3B_PN TRUE  // If true, use 3B page numbers.
 #endif
 
+// Prefer to use this function rather than #if FTLN_DEBUG directly because this will
+// ensure what's within will actually build whilst any reasonable compiler will ensure
+// the code is not actually included.
+static inline int ftln_debug() {
+  return FTLN_DEBUG;
+}
+
 //
 // Symbol Definitions.
 //
@@ -215,8 +222,7 @@
 #endif
   char vol_name[FTL_NAME_MAX];  // Volume name.
 
-  // Logger used by the FTL.
-  Logger logger;
+  FtlLogger logger;
 };
 
 __BEGIN_CDECLS
@@ -245,6 +251,10 @@
 int FtlnRecNeeded(CFTLN ftl, int wr_cnt);
 int FtlnRdPage(FTLN ftl, ui32 pn, void* buf);
 
+ui8 FtlnCalculateSpareValidity(const ui8* spare_buf, const ui8* data_buf, ui32 page_size);
+void FtlnSetSpareValidity(ui8* spare_buf, const ui8* data_buf, ui32 page_size);
+int FtlnCheckSpareValidity(const ui8* spare_bu, const ui8* data_buf, ui32 page_size);
+int FtlnIncompleteWrite(const ui8* spare_buf, const ui8* data_buf, ui32 page_size);
 int FtlnMapWr(void* vol, ui32 mpn, void* buf);
 int FtlnMetaWr(FTLN ftl, ui32 type);
 
diff --git a/zircon/system/ulib/ftl/ftln/ndm-driver.cc b/zircon/system/ulib/ftl/ftln/ndm-driver.cc
index ddc1609..2f42106 100644
--- a/zircon/system/ulib/ftl/ftln/ndm-driver.cc
+++ b/zircon/system/ulib/ftl/ftln/ndm-driver.cc
@@ -11,6 +11,7 @@
 
 #include "ftl.h"
 #include "ftl_private.h"
+#include "ftlnp.h"
 #include "ndm/ndmp.h"
 
 namespace ftl {
@@ -93,6 +94,10 @@
 int WritePages(uint32_t page, uint32_t count, const uint8_t* data, uint8_t* spare, int action,
                void* dev) {
   NdmDriver* device = reinterpret_cast<NdmDriver*>(dev);
+  for (uint32_t i = 0; i < count; i++) {
+    FtlnSetSpareValidity(&spare[i * device->SpareSize()], &data[i * device->PageSize()],
+                         device->PageSize());
+  }
   return device->NandWrite(page, count, data, spare);
 }
 
@@ -129,13 +134,14 @@
 // Returns kNdmOk or kNdmError, but kNdmError implies aborting initialization.
 int CheckPage(uint32_t page, uint8_t* data, uint8_t* spare, int* status, void* dev) {
   int result = ReadPagesImpl(page, 1, data, spare, dev);
+  NdmDriver* device = reinterpret_cast<NdmDriver*>(dev);
 
-  if (result == kNdmUncorrectableEcc || result == kNdmFatalError) {
+  if (result == kNdmUncorrectableEcc || result == kNdmFatalError ||
+      device->IncompletePageWrite(spare, data)) {
     *status = NDM_PAGE_INVALID;
     return kNdmOk;
   }
 
-  NdmDriver* device = reinterpret_cast<NdmDriver*>(dev);
   bool empty = device->IsEmptyPage(page, data, spare) ? kTrue : kFalse;
 
   *status = empty ? NDM_PAGE_ERASED : NDM_PAGE_VALID;
@@ -187,20 +193,18 @@
   fprintf(stderr, "\n");
 }
 
-Logger GetDefaultLogger() {
-  Logger logger = {
+}  // namespace
+
+FtlLogger DefaultLogger() {
+  return FtlLogger{
       .trace = &LogTrace,
       .debug = &LogDebug,
       .info = &LogInfo,
-      .warning = &LogWarning,
+      .warn = &LogWarning,
       .error = &LogError,
   };
-
-  return logger;
 }
 
-}  // namespace
-
 NdmBaseDriver::~NdmBaseDriver() { RemoveNdmVolume(); }
 
 bool NdmBaseDriver::IsNdmDataPresent(const VolumeOptions& options, bool use_format_v2) {
@@ -229,14 +233,6 @@
 
 const char* NdmBaseDriver::CreateNdmVolume(const Volume* ftl_volume, const VolumeOptions& options,
                                            bool save_volume_data) {
-  return CreateNdmVolumeWithLogger(ftl_volume, options, save_volume_data, std::nullopt);
-}
-
-const char* NdmBaseDriver::CreateNdmVolumeWithLogger(const Volume* ftl_volume,
-                                                     const VolumeOptions& options,
-                                                     bool save_volume_data,
-                                                     std::optional<LoggerProxy> logger) {
-  logger_ = logger;
   if (!ndm_) {
     IsNdmDataPresent(options, save_volume_data);
   }
@@ -254,7 +250,7 @@
   ftl.cached_map_pages = options.num_blocks * (options.block_size / options.page_size);
   ftl.extra_free = 6;  // Over-provision 6% of the device.
   xfs.ftl_volume = const_cast<Volume*>(ftl_volume);
-  ftl.logger = ndm_->logger;
+  ftl.logger = logger_;
 
   partition.exploded.basic_data.num_blocks = ndmGetNumVBlocks(ndm_);
   strncpy(partition.exploded.basic_data.name, "ftl", sizeof(partition.exploded.basic_data.name));
@@ -375,29 +371,15 @@
   driver->data_and_spare_check = CheckPage;
   driver->erase_block = EraseBlock;
   driver->is_block_bad = IsBadBlockImpl;
-  driver->logger = GetDefaultLogger();
+  driver->logger = logger_;
+}
 
-  if (logger_.has_value()) {
-    if (logger_->trace != nullptr) {
-      driver->logger.trace = logger_->trace;
-    }
+uint32_t NdmBaseDriver::PageSize() { return ndm_->page_size; }
 
-    if (logger_->debug != nullptr) {
-      driver->logger.debug = logger_->debug;
-    }
+uint8_t NdmBaseDriver::SpareSize() { return ndm_->eb_size; }
 
-    if (logger_->info != nullptr) {
-      driver->logger.info = logger_->info;
-    }
-
-    if (logger_->warn != nullptr) {
-      driver->logger.warning = logger_->warn;
-    }
-
-    if (logger_->error != nullptr) {
-      driver->logger.error = logger_->error;
-    }
-  }
+bool NdmBaseDriver::IncompletePageWrite(uint8_t* spare, uint8_t* data) {
+  return FtlnIncompleteWrite(spare, data, PageSize());
 }
 
 __EXPORT
diff --git a/zircon/system/ulib/ftl/ftln/stats.c b/zircon/system/ulib/ftl/ftln/stats.c
new file mode 100644
index 0000000..54c9e894
--- /dev/null
+++ b/zircon/system/ulib/ftl/ftln/stats.c
@@ -0,0 +1,103 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ftl.h"
+#include "ftlnp.h"
+
+// flush_bstat: Flush buffered statistics counts
+//
+//      Inputs: ftl = pointer to FTL control block
+//              b = block number of current block
+//              type = "FREE", "MAP", or "VOLUME"
+//  In/Outputs: *blk0 = first consecutive block number or -1
+//              *blke = end consecutive block number
+//
+static void flush_bstat(CFTLN ftl, int* blk0, int* blke, int b, const char* type) {
+  if (*blk0 == -1)
+    *blk0 = *blke = b;
+  else if (*blke + 1 == b)
+    *blke = b;
+  else {
+    printf("B = %4u", *blk0);
+    if (*blk0 == *blke) {
+      printf(" - used = %2u, wc lag = %3d, rc = %8u", NUM_USED(ftl->bdata[*blk0]),
+             ftl->blk_wc_lag[*blk0], GET_RC(ftl->bdata[*blk0]));
+      printf(" - %s BLOCK\n", type);
+    } else {
+      printf("-%-4u", *blke);
+      printf("%*s", 37, " ");
+      printf("- %s BLOCKS\n", type);
+    }
+    *blk0 = *blke = b;
+  }
+}
+
+// FtlnBlkStats: Debug function to display blocks statistics
+//
+//       Input: ftl = pointer to FTL control block
+//
+void FtlnBlkStats(CFTLN ftl) {
+  int free0 = -1, freee, vol0 = -1, vole;
+  ui32 b;
+
+  printf(
+      "\nBLOCK STATS: %u blocks, %u pages per block, curr free "
+      "blocks = %u\n",
+      ftl->num_blks, ftl->pgs_per_blk, ftl->num_free_blks);
+
+  // Loop over FTL blocks.
+  for (b = 0; b < ftl->num_blks; ++b) {
+    // Check if block is free.
+    if (IS_FREE(ftl->bdata[b])) {
+      flush_bstat(ftl, &vol0, &vole, -1, "VOLUME");
+      flush_bstat(ftl, &free0, &freee, b, "FREE");
+    }
+
+    // Else check if map block.
+    else if (IS_MAP_BLK(ftl->bdata[b])) {
+      flush_bstat(ftl, &free0, &freee, -1, "FREE");
+      flush_bstat(ftl, &vol0, &vole, -1, "VOLUME");
+      printf("B = %4u - used = %2u, wc lag = %3d, rc = %8u - ", b, NUM_USED(ftl->bdata[b]),
+             ftl->blk_wc_lag[b], GET_RC(ftl->bdata[b]));
+      printf("MAP BLOCK\n");
+    }
+
+    // Else is volume block.
+    else {
+      flush_bstat(ftl, &free0, &freee, -1, "FREE");
+      if (ftln_debug() <= 1) {
+        flush_bstat(ftl, &vol0, &vole, b, "VOLUME");
+      } else {
+        printf("B = %4u - used = %2u, wc lag = %3d, rc = %8u - ", b, NUM_USED(ftl->bdata[b]),
+               ftl->blk_wc_lag[b], GET_RC(ftl->bdata[b]));
+        printf("VOLUME BLOCK\n");
+      }
+    }
+  }
+  flush_bstat(ftl, &free0, &freee, -1, "FREE");
+  flush_bstat(ftl, &vol0, &vole, -1, "VOLUME");
+}
+
+//   FtlnStats: Display FTL statistics
+//
+//       Input: ftl = pointer to FTL control block
+//
+void FtlnStats(FTLN ftl) {
+  ui32 b, n;
+
+  printf("\nFTL STATS:\n");
+  printf("  - # vol pages    = %d\n", ftl->num_vpages);
+  printf("  - # map pages    = %d\n", ftl->num_map_pgs);
+  printf("  - # free blocks  = %d\n", ftl->num_free_blks);
+  for (n = b = 0; b < ftl->num_blks; ++b)
+    if (IS_ERASED(ftl->bdata[b]))
+      ++n;
+  printf("  - # erased blks  = %d\n", n);
+  printf("  - flags =");
+  if (ftl->flags & FTLN_FATAL_ERR)
+    printf(" FTLN_FATAL_ERR");
+  if (ftl->flags & FTLN_MOUNTED)
+    printf(" FTLN_MOUNTED");
+  putchar('\n');
+}
diff --git a/zircon/system/ulib/ftl/include/lib/ftl/logger.h b/zircon/system/ulib/ftl/include/lib/ftl/logger.h
new file mode 100644
index 0000000..e4cca5f
--- /dev/null
+++ b/zircon/system/ulib/ftl/include/lib/ftl/logger.h
@@ -0,0 +1,23 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_FTL_LOGGER_H_
+#define LIB_FTL_LOGGER_H_
+
+#include <zircon/compiler.h>
+
+__BEGIN_CDECLS
+
+// Helper for overriding default logging routines.
+typedef struct FtlLogger {
+  __PRINTFLIKE(3, 4) void (*trace)(const char*, int, const char*, ...);
+  __PRINTFLIKE(3, 4) void (*debug)(const char*, int, const char*, ...);
+  __PRINTFLIKE(3, 4) void (*info)(const char*, int, const char*, ...);
+  __PRINTFLIKE(3, 4) void (*warn)(const char*, int, const char*, ...);
+  __PRINTFLIKE(3, 4) void (*error)(const char*, int, const char*, ...);
+} FtlLogger;
+
+__END_CDECLS
+
+#endif  // LIB_FTL_LOGGER_H_
diff --git a/zircon/system/ulib/ftl/include/lib/ftl/ndm-driver.h b/zircon/system/ulib/ftl/include/lib/ftl/ndm-driver.h
index 1d0d12a..35cd643 100644
--- a/zircon/system/ulib/ftl/include/lib/ftl/ndm-driver.h
+++ b/zircon/system/ulib/ftl/include/lib/ftl/ndm-driver.h
@@ -5,6 +5,7 @@
 #ifndef LIB_FTL_NDM_DRIVER_H_
 #define LIB_FTL_NDM_DRIVER_H_
 
+#include <lib/ftl/logger.h>
 #include <zircon/compiler.h>
 
 #include <cstdint>
@@ -40,14 +41,7 @@
   uint32_t flags;
 };
 
-// Helper for overriding default logging routines.
-struct LoggerProxy {
-  __PRINTFLIKE(3, 4) void (*trace)(const char*, int, const char*, ...) = nullptr;
-  __PRINTFLIKE(3, 4) void (*debug)(const char*, int, const char*, ...) = nullptr;
-  __PRINTFLIKE(3, 4) void (*info)(const char*, int, const char*, ...) = nullptr;
-  __PRINTFLIKE(3, 4) void (*warn)(const char*, int, const char*, ...) = nullptr;
-  __PRINTFLIKE(3, 4) void (*error)(const char*, int, const char*, ...) = nullptr;
-};
+FtlLogger DefaultLogger();
 
 // Encapsulates the lower layer TargetFtl-Ndm driver.
 class __EXPORT NdmDriver {
@@ -91,12 +85,22 @@
   // Returns whether a given page is empty or not. |data| and |spare| store
   // the contents of the page.
   virtual bool IsEmptyPage(uint32_t page_num, const uint8_t* data, const uint8_t* spare) = 0;
+
+  // Returns the number of bytes in a page.
+  virtual uint32_t PageSize() = 0;
+
+  // Returns the number of bytes available for spare data storage.
+  virtual uint8_t SpareSize() = 0;
+
+  // Looks at the spare  and data buffer to try to determine if the write may
+  // have been incomplete. Return true if it appears to have been incomplete.
+  virtual bool IncompletePageWrite(uint8_t* spare, uint8_t* data) = 0;
 };
 
 // Base functionality for a driver implementation.
 class __EXPORT NdmBaseDriver : public NdmDriver {
  public:
-  NdmBaseDriver() {}
+  NdmBaseDriver(FtlLogger logger) : logger_(logger) {}
   virtual ~NdmBaseDriver();
 
   // Returns true if known data appears to be present on the device. This does
@@ -126,10 +130,6 @@
   const char* CreateNdmVolume(const Volume* ftl_volume, const VolumeOptions& options,
                               bool save_volume_data = true);
 
-  // Just like |CreateNdmVolume| but provides an override for default logging routines.
-  const char* CreateNdmVolumeWithLogger(const Volume* ftl_volume, const VolumeOptions& options,
-                                        bool save_volume_data, std::optional<LoggerProxy> logger);
-
   // Deletes the underlying NDM volume.
   bool RemoveNdmVolume();
 
@@ -155,6 +155,11 @@
   // save_volume_data set to true.
   bool WriteVolumeData();
 
+  bool IncompletePageWrite(uint8_t* spare, uint8_t* data) override;
+
+  uint32_t PageSize() override;
+  uint8_t SpareSize() override;
+
  protected:
   // This is exposed for unit tests only.
   ndm* GetNdmForTest() const { return ndm_; }
@@ -165,7 +170,7 @@
  private:
   ndm* ndm_ = nullptr;
   bool volume_data_saved_ = false;
-  std::optional<LoggerProxy> logger_ = std::nullopt;
+  const FtlLogger logger_;
 };
 
 // Performs global module initialization. This is exposed to support unit tests,
diff --git a/zircon/system/ulib/ftl/ndm/ndm_init.c b/zircon/system/ulib/ftl/ndm/ndm_init.c
index 8d03b61..2fd173d 100644
--- a/zircon/system/ulib/ftl/ndm/ndm_init.c
+++ b/zircon/system/ulib/ftl/ndm/ndm_init.c
@@ -984,7 +984,7 @@
 
   // Else device is NDM formatted. Find latest control information.
   if (find_last_ctrl_info(ndm)) {
-    ndm->logger.warning(__FILE__, __LINE__, "Failed to obtain valid NDM Control Block.");
+    ndm->logger.warn(__FILE__, __LINE__, "Failed to obtain valid NDM Control Block.");
     return -1;
   }
 
@@ -1096,7 +1096,7 @@
       FsError2(NDM_RD_ECC_FAIL, EIO);
       return 1;
     }
-    ndm->logger.error(__FILE__, __LINE__, "Failed to read page %d. IO Error.");
+    ndm->logger.error(__FILE__, __LINE__, "Failed to read page %d. IO Error.", old_pn);
     FsError2(NDM_EIO, EIO);
     return -2;
   }
diff --git a/zircon/system/ulib/ftl/ndm/ndmp.h b/zircon/system/ulib/ftl/ndm/ndmp.h
index 4a6782d..24385b5 100644
--- a/zircon/system/ulib/ftl/ndm/ndmp.h
+++ b/zircon/system/ulib/ftl/ndm/ndmp.h
@@ -155,7 +155,7 @@
   int (*erase_block)(ui32 pn, void* dev);
   int (*is_block_bad)(ui32 pn, void* dev);
 
-  Logger logger;
+  FtlLogger logger;
 
   // Device Dependent Variables
   void* dev;          // optional value set by driver
diff --git a/zircon/system/ulib/ftl/test/BUILD.gn b/zircon/system/ulib/ftl/test/BUILD.gn
index 322fd2f..236a66f 100644
--- a/zircon/system/ulib/ftl/test/BUILD.gn
+++ b/zircon/system/ulib/ftl/test/BUILD.gn
@@ -22,11 +22,13 @@
     }
   }
   sources = [
+    "ftl_test.cc",
     "ndm_driver_test.cc",
     "ndm_test.cc",
   ]
   deps = [
-    "//zircon/public/lib/zxtest",
+    "//src/lib/fxl/test:gtest_main",
+    "//third_party/googletest:gtest",
     "//zircon/system/ulib/ftl",
     "//zircon/system/ulib/ftl:private_headers",
   ]
diff --git a/zircon/system/ulib/ftl/test/ftl_test.cc b/zircon/system/ulib/ftl/test/ftl_test.cc
new file mode 100644
index 0000000..ad9ac69
--- /dev/null
+++ b/zircon/system/ulib/ftl/test/ftl_test.cc
@@ -0,0 +1,52 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <string.h>
+
+#include <gtest/gtest.h>
+
+#include "zircon/system/ulib/ftl/ftln/ftlnp.h"
+
+namespace {
+
+constexpr uint32_t kPageSize = 4096;
+constexpr uint8_t kOobSize = 16;
+
+TEST(FtlTest, IncompleteWriteWithValidity) {
+  uint8_t spare[kOobSize];
+  auto data = std::make_unique<uint8_t[]>(kPageSize);
+  memset(spare, 0xff, kOobSize);
+  memset(data.get(), 0xff, kPageSize);
+  data.get()[0] = 0;
+  FtlnSetSpareValidity(spare, data.get(), kPageSize);
+  ASSERT_FALSE(FtlnIncompleteWrite(spare, data.get(), kPageSize));
+}
+
+TEST(FtlTest, IncompleteWriteWithBadValidity) {
+  uint8_t spare[kOobSize];
+  auto data = std::make_unique<uint8_t[]>(kPageSize);
+  memset(spare, 0xff, kOobSize);
+  memset(data.get(), 0xff, kPageSize);
+  spare[14] = 0;
+  ASSERT_TRUE(FtlnIncompleteWrite(spare, data.get(), kPageSize));
+}
+
+TEST(FtlTest, IncompleteWriteNoValidityBadWearCount) {
+  uint8_t spare[kOobSize];
+  auto data = std::make_unique<uint8_t[]>(kPageSize);
+  memset(spare, 0xff, kOobSize);
+  memset(data.get(), 0xff, kPageSize);
+  ASSERT_TRUE(FtlnIncompleteWrite(spare, data.get(), kPageSize));
+}
+
+TEST(FtlTest, IncompleteWriteNoValidityGoodWearCount) {
+  uint8_t spare[kOobSize];
+  auto data = std::make_unique<uint8_t[]>(kPageSize);
+  memset(spare, 0xff, kOobSize);
+  memset(data.get(), 0xff, kPageSize);
+  spare[10] = 0;
+  ASSERT_FALSE(FtlnIncompleteWrite(spare, data.get(), kPageSize));
+}
+
+}  // namespace
diff --git a/zircon/system/ulib/ftl/test/ndm_driver_test.cc b/zircon/system/ulib/ftl/test/ndm_driver_test.cc
index 2573d9d..cd7e095 100644
--- a/zircon/system/ulib/ftl/test/ndm_driver_test.cc
+++ b/zircon/system/ulib/ftl/test/ndm_driver_test.cc
@@ -2,19 +2,19 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-#include "lib/ftl/ndm-driver.h"
-
-#include <zxtest/zxtest.h>
+#include <gtest/gtest.h>
 
 #include "ftl_private.h"
+#include "lib/ftl/ndm-driver.h"
 
 namespace {
 
 class MockDriver final : public ftl::NdmBaseDriver {
  public:
-  MockDriver() {}
+  MockDriver() : NdmBaseDriver(ftl::DefaultLogger()) {}
   ~MockDriver() final {}
 
+  void set_incomplete(bool value) { incomplete_ = value; }
   void set_result(int result) { result_ = result; }
   void set_empty(bool value) { empty_ = value; }
 
@@ -30,25 +30,27 @@
   int NandErase(uint32_t page_num) final;
   int IsBadBlock(uint32_t page_num) final { return ftl::kFalse; }
   bool IsEmptyPage(uint32_t page_num, const uint8_t* data, const uint8_t* spare) final;
+  bool IncompletePageWrite(uint8_t* spare, uint8_t* data) final { return incomplete_; }
+  uint32_t PageSize() final { return 4096; }
+  uint8_t SpareSize() final { return 16; }
 
  private:
   int result_ = ftl::kNdmOk;
   bool empty_ = true;
+  bool incomplete_ = false;
 };
 
 int MockDriver::NandRead(uint32_t start_page, uint32_t page_count, void* page_buffer,
-                           void* oob_buffer) {
+                         void* oob_buffer) {
   return result_;
 }
 
 int MockDriver::NandWrite(uint32_t start_page, uint32_t page_count, const void* page_buffer,
-                            const void* oob_buffer) {
+                          const void* oob_buffer) {
   return result_;
 }
 
-int MockDriver::NandErase(uint32_t page_num) {
-  return result_;
-}
+int MockDriver::NandErase(uint32_t page_num) { return result_; }
 
 bool MockDriver::IsEmptyPage(uint32_t page_num, const uint8_t* data, const uint8_t* spare) {
   return empty_;
@@ -59,7 +61,7 @@
 
   NDMDrvr ndm;
   driver.GetNdmDriver(&ndm);
-  ASSERT_NOT_NULL(ndm.data_and_spare_check);
+  ASSERT_NE(nullptr, ndm.data_and_spare_check);
 
   driver.set_result(ftl::kNdmUncorrectableEcc);
 
@@ -73,7 +75,7 @@
 
   NDMDrvr ndm;
   driver.GetNdmDriver(&ndm);
-  ASSERT_NOT_NULL(ndm.data_and_spare_check);
+  ASSERT_NE(nullptr, ndm.data_and_spare_check);
 
   driver.set_result(ftl::kNdmFatalError);
 
@@ -87,7 +89,7 @@
 
   NDMDrvr ndm;
   driver.GetNdmDriver(&ndm);
-  ASSERT_NOT_NULL(ndm.data_and_spare_check);
+  ASSERT_NE(nullptr, ndm.data_and_spare_check);
 
   int status;
   EXPECT_EQ(ftl::kNdmOk, ndm.data_and_spare_check(0, nullptr, nullptr, &status, &driver));
@@ -99,7 +101,7 @@
 
   NDMDrvr ndm;
   driver.GetNdmDriver(&ndm);
-  ASSERT_NOT_NULL(ndm.data_and_spare_check);
+  ASSERT_NE(nullptr, ndm.data_and_spare_check);
 
   driver.set_result(ftl::kNdmUnsafeEcc);
   driver.set_empty(false);
@@ -109,4 +111,19 @@
   EXPECT_EQ(NDM_PAGE_VALID, status);
 }
 
+TEST(NdmDriverTest, CheckPageValidIncompleteWrite) {
+  MockDriver driver;
+
+  NDMDrvr ndm;
+  driver.GetNdmDriver(&ndm);
+  ASSERT_NE(nullptr, ndm.data_and_spare_check);
+
+  driver.set_result(ftl::kNdmUnsafeEcc);
+  driver.set_incomplete(true);
+
+  int status;
+  EXPECT_EQ(ftl::kNdmOk, ndm.data_and_spare_check(0, nullptr, nullptr, &status, &driver));
+  EXPECT_EQ(NDM_PAGE_INVALID, status);
+}
+
 }  // namespace
diff --git a/zircon/system/ulib/ftl/test/ndm_test.cc b/zircon/system/ulib/ftl/test/ndm_test.cc
index 9967155c..2d98c92 100644
--- a/zircon/system/ulib/ftl/test/ndm_test.cc
+++ b/zircon/system/ulib/ftl/test/ndm_test.cc
@@ -3,11 +3,12 @@
 // found in the LICENSE file.
 
 #include <lib/ftl/ndm-driver.h>
+#include <string.h>
 
 #include <optional>
 #include <vector>
 
-#include <zxtest/zxtest.h>
+#include <gtest/gtest.h>
 
 #include "ftl.h"
 #include "ndm/ndmp.h"
@@ -27,17 +28,16 @@
 
 class NdmRamDriver final : public ftl::NdmBaseDriver {
  public:
-  NdmRamDriver(const ftl::VolumeOptions options = kDefaultOptions) : options_(options) {}
-  ~NdmRamDriver() final {}
+  NdmRamDriver(const ftl::VolumeOptions options = kDefaultOptions,
+               FtlLogger logger = ftl::DefaultLogger())
+      : NdmBaseDriver(logger), options_(options) {}
 
   const uint8_t* data(uint32_t page_num) const { return &volume_[page_num * kPageSize]; }
   NDM ndm() { return GetNdmForTest(); }
   void format_using_v2(bool value) { format_using_v2_ = value; }
 
   // Goes through the normal logic to create a volume with user data info.
-  const char* CreateVolume(std::optional<ftl::LoggerProxy> logger = std::nullopt) {
-    return CreateNdmVolumeWithLogger(nullptr, options_, true, logger);
-  }
+  const char* CreateVolume() { return CreateNdmVolume(nullptr, options_, true); }
 
   // NdmDriver interface:
   const char* Init() final;
@@ -51,6 +51,8 @@
   bool IsEmptyPage(uint32_t page_num, const uint8_t* data, const uint8_t* spare) final {
     return IsEmptyPageImpl(data, kPageSize, spare, kOobSize);
   }
+  uint32_t PageSize() final { return kPageSize; }
+  uint8_t SpareSize() final { return kOobSize; }
 
  private:
   std::vector<uint8_t> volume_;
@@ -113,12 +115,12 @@
   return ftl::kNdmOk;
 }
 
-class NdmTest : public zxtest::Test {
+class NdmTest : public ::testing::Test {
  public:
   void SetUp() override {
     ASSERT_TRUE(ftl::InitModules());
-    ASSERT_NULL(ndm_driver_.Init());
-    ASSERT_NULL(ndm_driver_.Attach(nullptr));
+    ASSERT_EQ(nullptr, ndm_driver_.Init());
+    ASSERT_EQ(nullptr, ndm_driver_.Attach(nullptr));
   }
 
  protected:
@@ -149,9 +151,9 @@
  public:
   void SetUp() override {
     ASSERT_TRUE(ftl::InitModules());
-    ASSERT_NULL(ndm_driver_.Init());
+    ASSERT_EQ(nullptr, ndm_driver_.Init());
     ndm_driver_.format_using_v2(false);
-    ASSERT_NULL(ndm_driver_.Attach(nullptr));
+    ASSERT_EQ(nullptr, ndm_driver_.Attach(nullptr));
   }
 };
 
@@ -160,12 +162,12 @@
   EXPECT_EQ(1, header->current_location);
   EXPECT_EQ(1, header->last_location);
   EXPECT_EQ(0, header->sequence_num);
-  EXPECT_EQ(kNumBlocks, header->num_blocks);
-  EXPECT_EQ(kPageSize * kPagesPerBlock, header->block_size);
-  EXPECT_EQ(kNumBlocks - 1, header->control_block0);
-  EXPECT_EQ(kNumBlocks - 2, header->control_block1);
-  EXPECT_EQ(kNumBlocks - 4, header->free_virt_block);
-  EXPECT_EQ(kNumBlocks - 3, header->free_control_block);
+  EXPECT_EQ(kNumBlocks, static_cast<uint32_t>(header->num_blocks));
+  EXPECT_EQ(kPageSize * kPagesPerBlock, static_cast<uint32_t>(header->block_size));
+  EXPECT_EQ(kNumBlocks - 1, static_cast<uint32_t>(header->control_block0));
+  EXPECT_EQ(kNumBlocks - 2, static_cast<uint32_t>(header->control_block1));
+  EXPECT_EQ(kNumBlocks - 4, static_cast<uint32_t>(header->free_virt_block));
+  EXPECT_EQ(kNumBlocks - 3, static_cast<uint32_t>(header->free_control_block));
   EXPECT_EQ(-1, header->transfer_to_block);
 }
 
@@ -176,12 +178,12 @@
   EXPECT_EQ(1, header->v1.current_location);
   EXPECT_EQ(1, header->v1.last_location);
   EXPECT_EQ(0, header->v1.sequence_num);
-  EXPECT_EQ(kNumBlocks, header->v1.num_blocks);
-  EXPECT_EQ(kPageSize * kPagesPerBlock, header->v1.block_size);
-  EXPECT_EQ(kNumBlocks - 1, header->v1.control_block0);
-  EXPECT_EQ(kNumBlocks - 2, header->v1.control_block1);
-  EXPECT_EQ(kNumBlocks - 4, header->v1.free_virt_block);
-  EXPECT_EQ(kNumBlocks - 3, header->v1.free_control_block);
+  EXPECT_EQ(kNumBlocks, static_cast<uint32_t>(header->v1.num_blocks));
+  EXPECT_EQ(kPageSize * kPagesPerBlock, static_cast<uint32_t>(header->v1.block_size));
+  EXPECT_EQ(kNumBlocks - 1, static_cast<uint32_t>(header->v1.control_block0));
+  EXPECT_EQ(kNumBlocks - 2, static_cast<uint32_t>(header->v1.control_block1));
+  EXPECT_EQ(kNumBlocks - 4, static_cast<uint32_t>(header->v1.free_virt_block));
+  EXPECT_EQ(kNumBlocks - 3, static_cast<uint32_t>(header->v1.free_control_block));
   EXPECT_EQ(-1, header->v1.transfer_to_block);
 }
 
@@ -198,8 +200,8 @@
   partition.num_blocks = ndmGetNumVBlocks(ndm_driver_.ndm());
   ASSERT_EQ(0, ndmWritePartition(ndm_driver_.ndm(), &partition, 0, "foo"));
 
-  EXPECT_NOT_NULL(ndmGetPartition(ndm_driver_.ndm(), 0));
-  EXPECT_NULL(ndmGetPartitionInfo(ndm_driver_.ndm()));
+  EXPECT_NE(nullptr, ndmGetPartition(ndm_driver_.ndm(), 0));
+  EXPECT_EQ(nullptr, ndmGetPartitionInfo(ndm_driver_.ndm()));
 }
 
 TEST_F(NdmTest, UsesVersion2) {
@@ -209,14 +211,14 @@
   strcpy(partition.basic_data.name, "foo");
   ASSERT_EQ(0, ndmWritePartitionInfo(ndm_driver_.ndm(), &partition));
 
-  EXPECT_NOT_NULL(ndmGetPartition(ndm_driver_.ndm(), 0));
+  EXPECT_NE(nullptr, ndmGetPartition(ndm_driver_.ndm(), 0));
 
   const NDMPartitionInfo* info = ndmGetPartitionInfo(ndm_driver_.ndm());
-  ASSERT_NOT_NULL(info);
-  EXPECT_EQ(0, info->basic_data.first_block);
+  ASSERT_NE(nullptr, info);
+  EXPECT_EQ(0u, info->basic_data.first_block);
   EXPECT_EQ(partition_size, info->basic_data.num_blocks);
-  EXPECT_EQ(0, info->user_data.data_size);
-  EXPECT_STR_EQ("foo", info->basic_data.name);
+  EXPECT_EQ(0u, info->user_data.data_size);
+  EXPECT_STREQ("foo", info->basic_data.name);
 }
 
 TEST_F(NdmTest, SavesVersion2) {
@@ -231,12 +233,12 @@
   EXPECT_EQ(1, header->v1.current_location);
   EXPECT_EQ(1, header->v1.last_location);
   EXPECT_EQ(1, header->v1.sequence_num);
-  EXPECT_EQ(kNumBlocks, header->v1.num_blocks);
-  EXPECT_EQ(kPageSize * kPagesPerBlock, header->v1.block_size);
-  EXPECT_EQ(kNumBlocks - 1, header->v1.control_block0);
-  EXPECT_EQ(kNumBlocks - 2, header->v1.control_block1);
-  EXPECT_EQ(kNumBlocks - 4, header->v1.free_virt_block);
-  EXPECT_EQ(kNumBlocks - 3, header->v1.free_control_block);
+  EXPECT_EQ(kNumBlocks, static_cast<uint32_t>(header->v1.num_blocks));
+  EXPECT_EQ(kPageSize * kPagesPerBlock, static_cast<uint32_t>(header->v1.block_size));
+  EXPECT_EQ(kNumBlocks - 1, static_cast<uint32_t>(header->v1.control_block0));
+  EXPECT_EQ(kNumBlocks - 2, static_cast<uint32_t>(header->v1.control_block1));
+  EXPECT_EQ(kNumBlocks - 4, static_cast<uint32_t>(header->v1.free_virt_block));
+  EXPECT_EQ(kNumBlocks - 3, static_cast<uint32_t>(header->v1.free_control_block));
   EXPECT_EQ(-1, header->v1.transfer_to_block);
 }
 
@@ -302,7 +304,7 @@
 
   // Reinitialize NDM.
   EXPECT_TRUE(ndm_driver_.Detach());
-  ASSERT_NULL(ndm_driver_.Attach(nullptr));
+  ASSERT_EQ(nullptr, ndm_driver_.Attach(nullptr));
 
   // Redefine the partition.
   PartitionInfo new_info = {};
@@ -314,14 +316,14 @@
 
   // Read the latest version from disk.
   EXPECT_TRUE(ndm_driver_.Detach());
-  ASSERT_NULL(ndm_driver_.Attach(nullptr));
+  ASSERT_EQ(nullptr, ndm_driver_.Attach(nullptr));
 
   const NDMPartitionInfo* info = ndmGetPartitionInfo(ndm_driver_.ndm());
-  ASSERT_NOT_NULL(info);
+  ASSERT_NE(nullptr, info);
   ASSERT_EQ(sizeof(new_info.exploded.data), info->user_data.data_size);
 
   auto actual_info = reinterpret_cast<const PartitionInfo*>(info);
-  EXPECT_EQ(42, actual_info->exploded.data);
+  EXPECT_EQ(42u, actual_info->exploded.data);
 
   // Verify the expected disk layout.
   auto header = reinterpret_cast<const HeaderV2*>(ndm_driver_.data(kControlPage0 + 1));
@@ -340,25 +342,25 @@
 }
 
 TEST_F(NdmTest, BaseDriverSavesConfig) {
-  ASSERT_NULL(ndm_driver_.CreateVolume());
+  ASSERT_EQ(nullptr, ndm_driver_.CreateVolume());
 
   const NDMPartitionInfo* info = ndmGetPartitionInfo(ndm_driver_.ndm());
-  ASSERT_NOT_NULL(info);
-  ASSERT_GE(info->user_data.data_size, 96);  // Size of the first version of the data.
+  ASSERT_NE(nullptr, info);
+  ASSERT_GE(info->user_data.data_size, 96u);  // Size of the first version of the data.
 
   const ftl::VolumeOptions* options = ndm_driver_.GetSavedOptions();
-  ASSERT_NOT_NULL(options);
-  ASSERT_BYTES_EQ(&kDefaultOptions, options, sizeof(*options));
+  ASSERT_NE(nullptr, options);
+  ASSERT_EQ(memcmp(&kDefaultOptions, options, sizeof(*options)), 0);
 }
 
-class NdmReadOnlyTest : public zxtest::Test {
+class NdmReadOnlyTest : public ::testing::Test {
  public:
   void SetUp() override {
     ASSERT_TRUE(ftl::InitModules());
     ftl::VolumeOptions options = kDefaultOptions;
     options.flags |= ftl::kReadOnlyInit;
     ndm_driver_.reset(new NdmRamDriver(options));
-    ASSERT_NULL(ndm_driver_->Init());
+    ASSERT_EQ(nullptr, ndm_driver_->Init());
     buffer_ = std::vector<uint8_t>(kPageSize, 0xff);
   }
 
@@ -378,7 +380,7 @@
   memcpy(buffer_.data(), kControl29V1, sizeof(kControl29V1));
   ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
 
-  ASSERT_NULL(ndm_driver_->CreateVolume());
+  ASSERT_EQ(nullptr, ndm_driver_->CreateVolume());
 }
 
 // An NDM control block version 2.0, stored on page 29.
@@ -390,7 +392,7 @@
   memcpy(buffer_.data(), kControl29V2, sizeof(kControl29V2));
   ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
 
-  ASSERT_NULL(ndm_driver_->CreateVolume());
+  ASSERT_EQ(nullptr, ndm_driver_->CreateVolume());
 }
 
 // An NDM control block version 2.0, stored on page 28, with partition data.
@@ -408,7 +410,7 @@
 
   memcpy(buffer_.data(), kControl28V2, sizeof(kControl28V2));
   ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage1, 1, buffer_.data(), kControlOob));
-  ASSERT_NULL(ndm_driver_->CreateVolume());
+  ASSERT_EQ(nullptr, ndm_driver_->CreateVolume());
 }
 
 // An NDM control block version 1, stored on page 29, with one factory bad block and
@@ -423,7 +425,7 @@
   memcpy(buffer_.data(), kControlBlockTransferV1, sizeof(kControlBlockTransferV1));
   ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
 
-  ASSERT_NOT_NULL(ndm_driver_->CreateVolume());
+  ASSERT_NE(nullptr, ndm_driver_->CreateVolume());
   ASSERT_EQ(NDM_BAD_BLK_RECOV, GetFsErrCode());
 }
 
@@ -438,8 +440,8 @@
   memcpy(buffer_.data(), kControlBlockBadBlocksV1, sizeof(kControlBlockBadBlocksV1));
   ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
 
-  ASSERT_NULL(ndm_driver_->CreateVolume());
-  EXPECT_EQ(2, ndm_driver_->ndm()->num_bad_blks);
+  ASSERT_EQ(nullptr, ndm_driver_->CreateVolume());
+  EXPECT_EQ(2u, ndm_driver_->ndm()->num_bad_blks);
 }
 
 // An NDM control block version 2.0, stored on page 29, with one factory bad block and
@@ -454,7 +456,7 @@
   memcpy(buffer_.data(), kControlBlockTransferV2, sizeof(kControlBlockTransferV2));
   ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
 
-  ASSERT_NOT_NULL(ndm_driver_->CreateVolume());
+  ASSERT_NE(nullptr, ndm_driver_->CreateVolume());
   ASSERT_EQ(NDM_BAD_BLK_RECOV, GetFsErrCode());
 }
 
@@ -466,7 +468,7 @@
     0x00000003, 0x0000001b, 0xffffffff, 0xffffffff, 0x00000000, 0x0000001a, 0x006c7466, 0x00000000,
     0x00000000, 0x00000000, 0x00000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff};
 
-TEST_F(NdmReadOnlyTest, BadBlocksV2) {
+TEST(NdmReadOnlyTestWithLogger, BadBlocksV2) {
   static bool logger_called = false;
   logger_called = false;
 
@@ -477,18 +479,25 @@
     }
   };
 
-  ftl::LoggerProxy logger;
+  FtlLogger logger;
   logger.trace = &LoggerHelper::Log;
   logger.debug = &LoggerHelper::Log;
   logger.info = &LoggerHelper::Log;
   logger.warn = &LoggerHelper::Log;
   logger.error = &LoggerHelper::Log;
 
-  memcpy(buffer_.data(), kControlBlockBadBlocksV2, sizeof(kControlBlockBadBlocksV2));
-  ASSERT_EQ(ftl::kNdmOk, ndm_driver_->NandWrite(kControlPage0, 1, buffer_.data(), kControlOob));
+  ASSERT_TRUE(ftl::InitModules());
+  ftl::VolumeOptions options = kDefaultOptions;
+  options.flags |= ftl::kReadOnlyInit;
+  auto ndm_driver = std::make_unique<NdmRamDriver>(options, logger);
+  ASSERT_EQ(nullptr, ndm_driver->Init());
+  std::vector<uint8_t> buffer(kPageSize, 0xff);
 
-  ASSERT_NULL(ndm_driver_->CreateVolume(logger));
-  EXPECT_EQ(2, ndm_driver_->ndm()->num_bad_blks);
+  memcpy(buffer.data(), kControlBlockBadBlocksV2, sizeof(kControlBlockBadBlocksV2));
+  ASSERT_EQ(ftl::kNdmOk, ndm_driver->NandWrite(kControlPage0, 1, buffer.data(), kControlOob));
+
+  ASSERT_EQ(nullptr, ndm_driver->CreateVolume());
+  EXPECT_EQ(2u, ndm_driver->ndm()->num_bad_blks);
   EXPECT_TRUE(logger_called);
 }