diff --git a/DEPS b/DEPS
index 02688a1..36374f70 100644
--- a/DEPS
+++ b/DEPS
@@ -54,7 +54,7 @@
   'checkout_telemetry_dependencies': False,
 
   # libaom provides support for AV1 but the bitstream is not frozen.
-  'checkout_libaom': True,
+  'checkout_libaom': False,
 
   # TODO(dpranke): change to != "small" once != is supported.
   'checkout_traffic_annotation_tools': 'checkout_configuration == "default"',
diff --git a/ash/BUILD.gn b/ash/BUILD.gn
index 939c7d1..9dad697 100644
--- a/ash/BUILD.gn
+++ b/ash/BUILD.gn
@@ -749,6 +749,8 @@
     "wallpaper/wallpaper_controller.cc",
     "wallpaper/wallpaper_controller.h",
     "wallpaper/wallpaper_controller_observer.h",
+    "wallpaper/wallpaper_decoder.cc",
+    "wallpaper/wallpaper_decoder.h",
     "wallpaper/wallpaper_delegate.h",
     "wallpaper/wallpaper_delegate_mus.cc",
     "wallpaper/wallpaper_delegate_mus.h",
diff --git a/ash/wallpaper/DEPS b/ash/wallpaper/DEPS
index f12a4015..f6a0335 100644
--- a/ash/wallpaper/DEPS
+++ b/ash/wallpaper/DEPS
@@ -1,3 +1,4 @@
 include_rules = [
   "+components/wallpaper",
+  "+services/data_decoder/public",
 ]
diff --git a/ash/wallpaper/wallpaper_decoder.cc b/ash/wallpaper/wallpaper_decoder.cc
new file mode 100644
index 0000000..39424416
--- /dev/null
+++ b/ash/wallpaper/wallpaper_decoder.cc
@@ -0,0 +1,48 @@
+// Copyright 2017 The Chromium 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 "ash/wallpaper/wallpaper_decoder.h"
+
+#include "ash/shell.h"
+#include "ash/shell_delegate.h"
+#include "base/sequenced_task_runner.h"
+#include "components/user_manager/user_image/user_image.h"
+#include "ipc/ipc_channel.h"
+#include "services/data_decoder/public/cpp/decode_image.h"
+
+namespace ash {
+namespace {
+
+const int64_t kMaxImageSizeInBytes =
+    static_cast<int64_t>(IPC::Channel::kMaximumMessageSize);
+
+// TODO(crbug.com/776464): This should be changed to |ConvertToImageSkia| after
+// the use of |UserImage| in |WallpaperManager| is deprecated.
+void ConvertToUserImage(OnWallpaperDecoded callback, const SkBitmap& image) {
+  SkBitmap final_image = image;
+  final_image.setImmutable();
+  gfx::ImageSkia image_skia = gfx::ImageSkia::CreateFrom1xBitmap(final_image);
+  image_skia.MakeThreadSafe();
+  auto user_image = std::make_unique<user_manager::UserImage>(
+      image_skia, new base::RefCountedBytes() /* unused */,
+      user_manager::UserImage::FORMAT_JPEG /* unused */);
+
+  std::move(callback).Run(std::move(user_image));
+}
+
+}  // namespace
+
+void DecodeWallpaper(std::unique_ptr<std::string> image_data,
+                     OnWallpaperDecoded callback) {
+  std::vector<uint8_t> image_bytes(image_data.get()->begin(),
+                                   image_data.get()->end());
+  data_decoder::DecodeImage(
+      Shell::Get()->shell_delegate()->GetShellConnector(),
+      std::move(image_bytes), data_decoder::mojom::ImageCodec::ROBUST_JPEG,
+      false /* shrink_to_fit */, kMaxImageSizeInBytes,
+      gfx::Size() /* desired_image_frame_size */,
+      base::BindOnce(&ConvertToUserImage, std::move(callback)));
+}
+
+}  // namespace ash
\ No newline at end of file
diff --git a/ash/wallpaper/wallpaper_decoder.h b/ash/wallpaper/wallpaper_decoder.h
new file mode 100644
index 0000000..5931f6e
--- /dev/null
+++ b/ash/wallpaper/wallpaper_decoder.h
@@ -0,0 +1,28 @@
+// Copyright 2017 The Chromium 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 ASH_WALLPAPER_WALLPAPER_DECODER_H_
+#define ASH_WALLPAPER_WALLPAPER_DECODER_H_
+
+#include "ash/ash_export.h"
+#include "base/callback_forward.h"
+#include "base/memory/scoped_refptr.h"
+
+namespace user_manager {
+class UserImage;
+}
+
+namespace ash {
+
+using OnWallpaperDecoded = base::OnceCallback<void(
+    std::unique_ptr<user_manager::UserImage> user_image)>;
+
+// Do an async wallpaper decode; |on_decoded| is run on the calling thread when
+// the decode has finished.
+ASH_EXPORT void DecodeWallpaper(std::unique_ptr<std::string> image_data,
+                                OnWallpaperDecoded callback);
+
+}  // namespace ash
+
+#endif  // ASH_WALLPAPER_WALLPAPER_DECODER_H_
\ No newline at end of file
diff --git a/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.cc b/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.cc
index b089fb04..9243eed6 100644
--- a/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.cc
+++ b/chrome/browser/chromeos/login/users/wallpaper/wallpaper_manager.cc
@@ -12,6 +12,7 @@
 #include "ash/public/cpp/window_properties.h"
 #include "ash/public/interfaces/constants.mojom.h"
 #include "ash/shell.h"
+#include "ash/wallpaper/wallpaper_decoder.h"
 #include "ash/wallpaper/wallpaper_window_state_manager.h"
 #include "base/bind.h"
 #include "base/bind_helpers.h"
@@ -258,6 +259,40 @@
   return written_bytes == size;
 }
 
+// If |data_is_ready| is true, start decoding the image, which will run
+// |callback| upon completion; if it's false, run |callback| immediately with
+// an empty image.
+// TODO(crbug.com/776464): Mash and some unit tests can't use this decoder
+// because it depends on the Shell instance. Make it work after all the decoding
+// code is moved to //ash.
+void DecodeWallpaperIfApplicable(user_image_loader::LoadedCallback callback,
+                                 std::unique_ptr<std::string> data,
+                                 bool data_is_ready) {
+  if (!data_is_ready) {
+    base::ThreadTaskRunnerHandle::Get()->PostTask(
+        FROM_HERE,
+        base::BindOnce(
+            std::move(callback),
+            base::Passed(std::make_unique<user_manager::UserImage>())));
+    return;
+  }
+  ash::DecodeWallpaper(std::move(data), std::move(callback));
+}
+
+// Reads image from |file_path| on disk, and calls |DecodeWallpaperIfApplicable|
+// with the result of |ReadFileToString|.
+void ReadAndDecodeWallpaper(
+    user_image_loader::LoadedCallback callback,
+    scoped_refptr<base::SequencedTaskRunner> task_runner,
+    const base::FilePath& file_path) {
+  std::string* data = new std::string;
+  base::PostTaskAndReplyWithResult(
+      task_runner.get(), FROM_HERE,
+      base::Bind(&base::ReadFileToString, file_path, data),
+      base::Bind(&DecodeWallpaperIfApplicable, std::move(callback),
+                 base::Passed(base::WrapUnique(data))));
+}
+
 }  // namespace
 
 void AssertCalledOnWallpaperSequence(base::SequencedTaskRunner* task_runner) {
@@ -1029,11 +1064,18 @@
   if (!data)
     return;
 
-  user_image_loader::StartWithData(
-      task_runner_, std::move(data), ImageDecoder::ROBUST_JPEG_CODEC,
-      0,  // Do not crop.
-      base::Bind(&WallpaperManager::SetPolicyControlledWallpaper,
-                 weak_factory_.GetWeakPtr(), account_id));
+  if (!ash::Shell::HasInstance() || ash_util::IsRunningInMash()) {
+    user_image_loader::StartWithData(
+        task_runner_, std::move(data), ImageDecoder::ROBUST_JPEG_CODEC,
+        0,  // Do not crop.
+        base::Bind(&WallpaperManager::SetPolicyControlledWallpaper,
+                   weak_factory_.GetWeakPtr(), account_id));
+  } else {
+    DecodeWallpaperIfApplicable(
+        base::Bind(&WallpaperManager::SetPolicyControlledWallpaper,
+                   weak_factory_.GetWeakPtr(), account_id),
+        std::move(data), true /* data_is_ready */);
+  }
 }
 
 size_t WallpaperManager::GetPendingListSizeForTesting() const {
@@ -1749,12 +1791,21 @@
     (*GetWallpaperCacheMap())[account_id] =
         ash::CustomWallpaperElement(wallpaper_path, gfx::ImageSkia());
   }
-  user_image_loader::StartWithFilePath(
-      task_runner_, wallpaper_path, ImageDecoder::ROBUST_JPEG_CODEC,
-      0,  // Do not crop.
-      base::Bind(&WallpaperManager::OnWallpaperDecoded,
-                 weak_factory_.GetWeakPtr(), account_id, info, update_wallpaper,
-                 base::Passed(std::move(on_finish))));
+
+  if (!ash::Shell::HasInstance() || ash_util::IsRunningInMash()) {
+    user_image_loader::StartWithFilePath(
+        task_runner_, wallpaper_path, ImageDecoder::ROBUST_JPEG_CODEC,
+        0,  // Do not crop.
+        base::Bind(&WallpaperManager::OnWallpaperDecoded,
+                   weak_factory_.GetWeakPtr(), account_id, info,
+                   update_wallpaper, base::Passed(std::move(on_finish))));
+  } else {
+    ReadAndDecodeWallpaper(
+        base::Bind(&WallpaperManager::OnWallpaperDecoded,
+                   weak_factory_.GetWeakPtr(), account_id, info,
+                   update_wallpaper, base::Passed(std::move(on_finish))),
+        task_runner_, wallpaper_path);
+  }
 }
 
 void WallpaperManager::SaveLastLoadTime(const base::TimeDelta elapsed) {
@@ -1811,13 +1862,21 @@
     DCHECK(rescaled_files->downloaded_exists());
 
     // Either resized images do not exist or cached version is incorrect.
-    // Need to start resize again.
-    user_image_loader::StartWithFilePath(
-        task_runner_, file_path, ImageDecoder::ROBUST_JPEG_CODEC,
-        0,  // Do not crop.
-        base::Bind(&WallpaperManager::OnCustomizedDefaultWallpaperDecoded,
-                   weak_factory_.GetWeakPtr(), wallpaper_url,
-                   base::Passed(std::move(rescaled_files))));
+    // Need to start decoding again.
+    if (!ash::Shell::HasInstance() || ash_util::IsRunningInMash()) {
+      user_image_loader::StartWithFilePath(
+          task_runner_, file_path, ImageDecoder::ROBUST_JPEG_CODEC,
+          0,  // Do not crop.
+          base::Bind(&WallpaperManager::OnCustomizedDefaultWallpaperDecoded,
+                     weak_factory_.GetWeakPtr(), wallpaper_url,
+                     base::Passed(std::move(rescaled_files))));
+    } else {
+      ReadAndDecodeWallpaper(
+          base::Bind(&WallpaperManager::OnCustomizedDefaultWallpaperDecoded,
+                     weak_factory_.GetWeakPtr(), wallpaper_url,
+                     base::Passed(std::move(rescaled_files))),
+          task_runner_, file_path);
+    }
   } else {
     SetDefaultWallpaperPath(rescaled_files->path_rescaled_small(),
                             std::unique_ptr<gfx::ImageSkia>(),
@@ -1918,6 +1977,11 @@
   }
 
   *result_out = std::move(user_image);
+  // Make sure the file path is updated.
+  // TODO(crbug.com/776464): Use |ImageSkia| and |FilePath| directly to cache
+  // the decoded image and file path. Nothing else in |UserImage| is relevant.
+  (*result_out)->set_file_path(path);
+
   if (update_wallpaper) {
     WallpaperInfo info(path.value(), layout, wallpaper::DEFAULT,
                        base::Time::Now().LocalMidnight());
@@ -1931,13 +1995,22 @@
     bool update_wallpaper,
     MovableOnDestroyCallbackHolder on_finish,
     std::unique_ptr<user_manager::UserImage>* result_out) {
-  user_image_loader::StartWithFilePath(
-      task_runner_, path, ImageDecoder::ROBUST_JPEG_CODEC,
-      0,  // Do not crop.
-      base::Bind(&WallpaperManager::OnDefaultWallpaperDecoded,
-                 weak_factory_.GetWeakPtr(), path, layout, update_wallpaper,
-                 base::Unretained(result_out),
-                 base::Passed(std::move(on_finish))));
+  if (!ash::Shell::HasInstance() || ash_util::IsRunningInMash()) {
+    user_image_loader::StartWithFilePath(
+        task_runner_, path, ImageDecoder::ROBUST_JPEG_CODEC,
+        0,  // Do not crop.
+        base::Bind(&WallpaperManager::OnDefaultWallpaperDecoded,
+                   weak_factory_.GetWeakPtr(), path, layout, update_wallpaper,
+                   base::Unretained(result_out),
+                   base::Passed(std::move(on_finish))));
+  } else {
+    ReadAndDecodeWallpaper(
+        base::Bind(&WallpaperManager::OnDefaultWallpaperDecoded,
+                   weak_factory_.GetWeakPtr(), path, layout, update_wallpaper,
+                   base::Unretained(result_out),
+                   base::Passed(std::move(on_finish))),
+        task_runner_, path);
+  }
 }
 
 void WallpaperManager::RecordWallpaperAppType() {
@@ -2122,12 +2195,20 @@
     return;
   }
 
-  user_image_loader::StartWithFilePath(
-      task_runner_, GetDeviceWallpaperFilePath(),
-      ImageDecoder::ROBUST_JPEG_CODEC,
-      0,  // Do not crop.
-      base::Bind(&WallpaperManager::OnDeviceWallpaperDecoded,
-                 weak_factory_.GetWeakPtr(), account_id));
+  const base::FilePath file_path = GetDeviceWallpaperFilePath();
+  if (!ash::Shell::HasInstance() || ash_util::IsRunningInMash()) {
+    user_image_loader::StartWithFilePath(
+        task_runner_, GetDeviceWallpaperFilePath(),
+        ImageDecoder::ROBUST_JPEG_CODEC,
+        0,  // Do not crop.
+        base::Bind(&WallpaperManager::OnDeviceWallpaperDecoded,
+                   weak_factory_.GetWeakPtr(), account_id));
+  } else {
+    ReadAndDecodeWallpaper(
+        base::Bind(&WallpaperManager::OnDeviceWallpaperDecoded,
+                   weak_factory_.GetWeakPtr(), account_id),
+        task_runner_, file_path);
+  }
 }
 
 void WallpaperManager::OnDeviceWallpaperDecoded(
diff --git a/chrome/browser/resources/cryptotoken/devicestatuscodes.js b/chrome/browser/resources/cryptotoken/devicestatuscodes.js
index 165102a..a15acd4 100644
--- a/chrome/browser/resources/cryptotoken/devicestatuscodes.js
+++ b/chrome/browser/resources/cryptotoken/devicestatuscodes.js
@@ -45,6 +45,12 @@
 DeviceStatusCodes.WRONG_DATA_STATUS = 0x6a80;
 
 /**
+ * Device operation file not found status.
+ * @const
+ */
+DeviceStatusCodes.FILE_NOT_FOUND_STATUS = 0x6a82;
+
+/**
  * Device operation timeout status.
  * @const
  */
diff --git a/chrome/browser/resources/cryptotoken/enroller.js b/chrome/browser/resources/cryptotoken/enroller.js
index 94ae1d4..2ddd25c 100644
--- a/chrome/browser/resources/cryptotoken/enroller.js
+++ b/chrome/browser/resources/cryptotoken/enroller.js
@@ -539,7 +539,7 @@
  * Notifies the caller of success with the provided response data.
  * @param {string} u2fVersion Protocol version
  * @param {string} info Response data
- * @param {string|undefined} opt_browserData Browser data used
+ * @param {string=} opt_browserData Browser data used
  * @private
  */
 Enroller.prototype.notifySuccess_ = function(
@@ -562,6 +562,12 @@
     console.log(UTIL_fmt(
         'helper reported ' + reply.code.toString(16) + ', returning ' +
         reportedError.errorCode));
+    // Log non-expected reply codes if we have url to send them.
+    if (reportedError.errorCode == ErrorCodes.OTHER_ERROR) {
+      var logMsg = 'log=u2fenroll&rc=' + reply.code.toString(16);
+      if (this.logMsgUrl_)
+        logMessage(logMsg, this.logMsgUrl_);
+    }
     this.notifyError_(reportedError);
   } else {
     console.log(UTIL_fmt('Gnubby enrollment succeeded!!!!!'));
diff --git a/chrome/browser/resources/cryptotoken/gnubbies.js b/chrome/browser/resources/cryptotoken/gnubbies.js
index d6c10791..47ac315 100644
--- a/chrome/browser/resources/cryptotoken/gnubbies.js
+++ b/chrome/browser/resources/cryptotoken/gnubbies.js
@@ -170,7 +170,7 @@
     }
 
     console.log(UTIL_fmt('Enumerated ' + devs.length + ' gnubbies'));
-    console.log(devs);
+    console.log(UTIL_fmt(JSON.stringify(devs)));
 
     var presentDevs = {};
     var deviceIds = [];
diff --git a/chrome/browser/resources/cryptotoken/gnubby.js b/chrome/browser/resources/cryptotoken/gnubby.js
index 28910ce..d11df22 100644
--- a/chrome/browser/resources/cryptotoken/gnubby.js
+++ b/chrome/browser/resources/cryptotoken/gnubby.js
@@ -66,10 +66,10 @@
 /**
  * Opens the gnubby with the given index, or the first found gnubby if no
  * index is specified.
- * @param {GnubbyDeviceId} which The device to open. If null, the first
+ * @param {?GnubbyDeviceId} which The device to open. If null, the first
  *     gnubby found is opened.
  * @param {GnubbyEnumerationTypes=} opt_type Which type of device to enumerate.
- * @param {function(number)|undefined} opt_cb Called with result of opening the
+ * @param {function(number)=} opt_cb Called with result of opening the
  *     gnubby.
  * @param {string=} opt_caller Identifier for the caller.
  */
@@ -113,22 +113,23 @@
       if (self.closeHook_) {
         self.dev.setDestroyHook(self.closeHook_);
       }
-      cb(rc);
+      cb.call(self, rc);
     });
   }
 
   if (which) {
     setCid(which);
     self.which = which;
-    Gnubby.gnubbies_.addClient(which, self, function(rc, device) {
-      if (!rc) {
-        self.dev = device;
-        if (self.closeHook_) {
-          self.dev.setDestroyHook(self.closeHook_);
-        }
-      }
-      cb(rc);
-    });
+    Gnubby.gnubbies_.addClient(
+        /** @type {GnubbyDeviceId} */ (which), self, function(rc, device) {
+          if (!rc) {
+            self.dev = device;
+            if (self.closeHook_) {
+              self.dev.setDestroyHook(self.closeHook_);
+            }
+          }
+          cb.call(self, rc);
+        });
   } else {
     Gnubby.gnubbies_.enumerate(enumerated, opt_type);
   }
@@ -139,7 +140,7 @@
  * collide within this application, but may when others simultaneously access
  * the device.
  * @param {number} gnubbyInstance An instance identifier for a gnubby.
- * @param {GnubbyDeviceId} which The device identifer for the gnubby device.
+ * @param {GnubbyDeviceId} which The device identifier for the gnubby device.
  * @return {number} The channel id.
  * @private
  */
@@ -497,9 +498,8 @@
  * @param {ArrayBuffer|Uint8Array} data Command data
  * @param {number} timeout Timeout in seconds.
  * @param {function(number, ArrayBuffer=)} cb Callback
- * @private
  */
-Gnubby.prototype.exchange_ = function(cmd, data, timeout, cb) {
+Gnubby.prototype.exchange = function(cmd, data, timeout, cb) {
   var busyWait = new CountdownTimer(Gnubby.SYS_TIMER_, this.busyMillis);
   var self = this;
 
@@ -742,7 +742,7 @@
     var d = new Uint8Array([data]);
     data = d.buffer;
   }
-  this.exchange_(GnubbyDevice.CMD_PROMPT, data, Gnubby.NORMAL_TIMEOUT, cb);
+  this.exchange(GnubbyDevice.CMD_PROMPT, data, Gnubby.NORMAL_TIMEOUT, cb);
 };
 
 /** Lock the gnubby
@@ -756,7 +756,7 @@
     var d = new Uint8Array([data]);
     data = d.buffer;
   }
-  this.exchange_(GnubbyDevice.CMD_LOCK, data, Gnubby.NORMAL_TIMEOUT, cb);
+  this.exchange(GnubbyDevice.CMD_LOCK, data, Gnubby.NORMAL_TIMEOUT, cb);
 };
 
 /** Unlock the gnubby
@@ -766,7 +766,7 @@
   if (!cb)
     cb = Gnubby.defaultCallback;
   var data = new Uint8Array([0]);
-  this.exchange_(GnubbyDevice.CMD_LOCK, data.buffer, Gnubby.NORMAL_TIMEOUT, cb);
+  this.exchange(GnubbyDevice.CMD_LOCK, data.buffer, Gnubby.NORMAL_TIMEOUT, cb);
 };
 
 /** Request system information data.
@@ -775,7 +775,7 @@
 Gnubby.prototype.sysinfo = function(cb) {
   if (!cb)
     cb = Gnubby.defaultCallback;
-  this.exchange_(
+  this.exchange(
       GnubbyDevice.CMD_SYSINFO, new ArrayBuffer(0), Gnubby.NORMAL_TIMEOUT, cb);
 };
 
@@ -785,7 +785,7 @@
 Gnubby.prototype.wink = function(cb) {
   if (!cb)
     cb = Gnubby.defaultCallback;
-  this.exchange_(
+  this.exchange(
       GnubbyDevice.CMD_WINK, new ArrayBuffer(0), Gnubby.NORMAL_TIMEOUT, cb);
 };
 
@@ -796,7 +796,7 @@
 Gnubby.prototype.dfu = function(data, cb) {
   if (!cb)
     cb = Gnubby.defaultCallback;
-  this.exchange_(GnubbyDevice.CMD_DFU, data, Gnubby.NORMAL_TIMEOUT, cb);
+  this.exchange(GnubbyDevice.CMD_DFU, data, Gnubby.NORMAL_TIMEOUT, cb);
 };
 
 /** Ping the gnubby
@@ -811,7 +811,7 @@
     window.crypto.getRandomValues(d);
     data = d.buffer;
   }
-  this.exchange_(GnubbyDevice.CMD_PING, data, Gnubby.NORMAL_TIMEOUT, cb);
+  this.exchange(GnubbyDevice.CMD_PING, data, Gnubby.NORMAL_TIMEOUT, cb);
 };
 
 /** Send a raw APDU command
@@ -821,7 +821,7 @@
 Gnubby.prototype.apdu = function(data, cb) {
   if (!cb)
     cb = Gnubby.defaultCallback;
-  this.exchange_(GnubbyDevice.CMD_APDU, data, Gnubby.MAX_TIMEOUT, cb);
+  this.exchange(GnubbyDevice.CMD_APDU, data, Gnubby.MAX_TIMEOUT, cb);
 };
 
 /** Reset gnubby
@@ -830,7 +830,7 @@
 Gnubby.prototype.reset = function(cb) {
   if (!cb)
     cb = Gnubby.defaultCallback;
-  this.exchange_(
+  this.exchange(
       GnubbyDevice.CMD_ATR, new ArrayBuffer(0), Gnubby.MAX_TIMEOUT, cb);
 };
 
@@ -845,7 +845,7 @@
   if (!cb)
     cb = Gnubby.defaultCallback;
   var u8 = new Uint8Array(args);
-  this.exchange_(
+  this.exchange(
       GnubbyDevice.CMD_USB_TEST, u8.buffer, Gnubby.NORMAL_TIMEOUT, cb);
 };
 
diff --git a/chrome/browser/resources/cryptotoken/gnubbyfactory.js b/chrome/browser/resources/cryptotoken/gnubbyfactory.js
index aca6c6d..780afc3 100644
--- a/chrome/browser/resources/cryptotoken/gnubbyfactory.js
+++ b/chrome/browser/resources/cryptotoken/gnubbyfactory.js
@@ -50,3 +50,13 @@
  */
 GnubbyFactory.prototype.notEnrolledPrerequisiteCheck = function(
     gnubby, appIdHash, cb) {};
+
+/**
+ * Called immediately after enrolling the gnubby to perform necessary actions.
+ * @param {Gnubby} gnubby The just-enrolled gnubby.
+ * @param {string} appIdHash The base64-encoded hash of the app id for which
+ *     the gnubby was enrolled.
+ * @param {FactoryOpenCallback} cb Called with the result of the action.
+ *     (A non-zero status indicates failure.)
+ */
+GnubbyFactory.prototype.postEnrollAction = function(gnubby, appIdHash, cb) {};
diff --git a/chrome/browser/resources/cryptotoken/logging.js b/chrome/browser/resources/cryptotoken/logging.js
index 07462f8..9054731 100644
--- a/chrome/browser/resources/cryptotoken/logging.js
+++ b/chrome/browser/resources/cryptotoken/logging.js
@@ -11,7 +11,7 @@
  * @param {string=} opt_logMsgUrl the url to post log messages to.
  */
 function logMessage(logMsg, opt_logMsgUrl) {
-  console.log(UTIL_fmt('logMessage("' + logMsg + '")'));
+  console.warn(UTIL_fmt('logMessage("' + logMsg + '")'));
 
   if (!opt_logMsgUrl) {
     return;
diff --git a/chrome/browser/resources/cryptotoken/manifest.json b/chrome/browser/resources/cryptotoken/manifest.json
index b0dbce6..62614d2 100644
--- a/chrome/browser/resources/cryptotoken/manifest.json
+++ b/chrome/browser/resources/cryptotoken/manifest.json
@@ -1,7 +1,7 @@
 {
   "name": "CryptoTokenExtension",
   "description": "CryptoToken Component Extension",
-  "version": "0.9.46",
+  "version": "0.9.71",
   "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq7zRobvA+AVlvNqkHSSVhh1sEWsHSqz4oR/XptkDe/Cz3+gW9ZGumZ20NCHjaac8j1iiesdigp8B1LJsd/2WWv2Dbnto4f8GrQ5MVphKyQ9WJHwejEHN2K4vzrTcwaXqv5BSTXwxlxS/mXCmXskTfryKTLuYrcHEWK8fCHb+0gvr8b/kvsi75A1aMmb6nUnFJvETmCkOCPNX5CHTdy634Ts/x0fLhRuPlahk63rdf7agxQv5viVjQFk+tbgv6aa9kdSd11Js/RZ9yZjrFgHOBWgP4jTBqud4+HUglrzu8qynFipyNRLCZsaxhm+NItTyNgesxLdxZcwOz56KD1Q4IQIDAQAB",
   "manifest_version": 2,
   "permissions": [
diff --git a/chrome/browser/resources/cryptotoken/multiplesigner.js b/chrome/browser/resources/cryptotoken/multiplesigner.js
index 1fa9903..748f404 100644
--- a/chrome/browser/resources/cryptotoken/multiplesigner.js
+++ b/chrome/browser/resources/cryptotoken/multiplesigner.js
@@ -52,7 +52,7 @@
   /** @private {string|undefined} */
   this.logMsgUrl_ = opt_logMsgUrl;
 
-  /** @private {Array<SignHelperChallenge>} */
+  /** @private {Array<DecodedSignHelperChallenge>} */
   this.challenges_ = [];
   /** @private {boolean} */
   this.challengesSet_ = false;
@@ -107,12 +107,12 @@
 
   if (challenges) {
     for (var i = 0; i < challenges.length; i++) {
-      var decodedChallenge = {};
       var challenge = challenges[i];
-      decodedChallenge['challengeHash'] =
-          B64_decode(challenge['challengeHash']);
-      decodedChallenge['appIdHash'] = B64_decode(challenge['appIdHash']);
-      decodedChallenge['keyHandle'] = B64_decode(challenge['keyHandle']);
+      var decodedChallenge = {
+        challengeHash: B64_decode(challenge['challengeHash']),
+        appIdHash: B64_decode(challenge['appIdHash']),
+        keyHandle: B64_decode(challenge['keyHandle'])
+      };
       if (challenge['version']) {
         decodedChallenge['version'] = challenge['version'];
       }
diff --git a/chrome/browser/resources/cryptotoken/requestqueue.js b/chrome/browser/resources/cryptotoken/requestqueue.js
index 356a777..d283ad8 100644
--- a/chrome/browser/resources/cryptotoken/requestqueue.js
+++ b/chrome/browser/resources/cryptotoken/requestqueue.js
@@ -33,8 +33,10 @@
   this.queue_ = queue;
   /** @private {number} */
   this.id_ = id;
-  /** @type {function(QueuedRequestToken)} */
-  this.beginCb = beginCb;
+  /** @private {boolean} */
+  this.begun_ = false;
+  /** @private {function(QueuedRequestToken)} */
+  this.beginCb_ = beginCb;
   /** @type {RequestToken} */
   this.prev = null;
   /** @type {RequestToken} */
@@ -43,6 +45,17 @@
   this.completed_ = false;
 }
 
+/** Begins work on this queued request. */
+RequestToken.prototype.begin = function() {
+  this.begun_ = true;
+  this.beginCb_(this);
+};
+
+/** @return {boolean} Whether this token has already begun. */
+RequestToken.prototype.begun = function() {
+  return this.begun_;
+};
+
 /** Completes (or cancels) this queued request. */
 RequestToken.prototype.complete = function() {
   if (this.completed_) {
@@ -59,6 +72,12 @@
   return this.completed_;
 };
 
+/** @return {number} This token's id. */
+RequestToken.prototype.id = function() {
+  return this.id_;
+};
+
+
 /**
  * @param {!SystemTimer} sysTimer A system timer implementation.
  * @constructor
@@ -96,15 +115,32 @@
 /**
  * Removes this token from the queue.
  * @param {RequestToken} token Queue token
+ * @return {RequestToken?} The next token in the queue to run, if any.
  * @private
  */
 RequestQueue.prototype.removeToken_ = function(token) {
+  var nextTokenToRun = null;
+  // If this token has been begun, find the next token to run.
+  if (token.begun()) {
+    // Find the first token in the queue which has not yet been begun, and which
+    // is not the token being removed.
+    for (var nextToken = this.head_; nextToken; nextToken = nextToken.next) {
+      if (nextToken !== token && !nextToken.begun()) {
+        nextTokenToRun = nextToken;
+        break;
+      }
+    }
+  }
+
+  // Remove this token from the queue
   if (token.next) {
     token.next.prev = token.prev;
   }
   if (token.prev) {
     token.prev.next = token.next;
   }
+
+  // Update head and tail of queue.
   if (this.head_ === token && this.tail_ === token) {
     this.head_ = this.tail_ = null;
   } else {
@@ -117,7 +153,12 @@
       this.tail_.next = null;
     }
   }
+
+  // Isolate this token to prevent it from manipulating the queue, e.g. if
+  // complete() is called a second time with it.
   token.prev = token.next = null;
+
+  return nextTokenToRun;
 };
 
 /**
@@ -126,11 +167,16 @@
  * @param {RequestToken} token Queue token
  */
 RequestQueue.prototype.complete = function(token) {
-  console.log(UTIL_fmt('token ' + this.id_ + ' completed'));
-  var next = token.next;
-  this.removeToken_(token);
+  var next = this.removeToken_(token);
   if (next) {
-    next.beginCb(next);
+    console.log(
+        UTIL_fmt('token ' + token.id() + ' completed, starting ' + next.id()));
+    next.begin();
+  } else if (this.empty()) {
+    console.log(UTIL_fmt('token ' + token.id() + ' completed, queue empty'));
+  } else {
+    console.log(UTIL_fmt(
+        'token ' + token.id() + ' completed (earlier token still running)'));
   }
 };
 
@@ -156,7 +202,7 @@
   if (startNow) {
     this.sysTimer_.setTimeout(function() {
       if (!token.completed()) {
-        token.beginCb(token);
+        token.begin();
       }
     }, 0);
   }
diff --git a/chrome/browser/resources/cryptotoken/signer.js b/chrome/browser/resources/cryptotoken/signer.js
index 5a887719..8c8352f 100644
--- a/chrome/browser/resources/cryptotoken/signer.js
+++ b/chrome/browser/resources/cryptotoken/signer.js
@@ -11,6 +11,9 @@
 
 var gnubbySignRequestQueue;
 
+/**
+ * Initialize request queue.
+ */
 function initRequestQueue() {
   gnubbySignRequestQueue =
       new OriginKeyedRequestQueue(FACTORY_REGISTRY.getSystemTimer());
@@ -177,10 +180,10 @@
  * @param {WebRequestSender} sender Message sender.
  * @param {function(U2fError)} errorCb Error callback
  * @param {function(SignChallenge, string, string)} successCb Success callback
- * @param {string|undefined} opt_defaultChallenge A default sign challenge
+ * @param {string=} opt_defaultChallenge A default sign challenge
  *     value, if a request does not provide one.
- * @param {string|undefined} opt_appId The app id for the entire request.
- * @param {string|undefined} opt_logMsgUrl Url to post log messages to
+ * @param {string=} opt_appId The app id for the entire request.
+ * @param {string=} opt_logMsgUrl Url to post log messages to
  * @constructor
  * @implements {Closeable}
  */
@@ -538,6 +541,12 @@
     console.log(UTIL_fmt(
         'helper reported ' + reply.code.toString(16) + ', returning ' +
         reportedError.errorCode));
+    // Log non-expected reply codes if we have an url to send them
+    if (reportedError.errorCode == ErrorCodes.OTHER_ERROR) {
+      var logMsg = 'log=u2fsign&rc=' + reply.code.toString(16);
+      if (this.logMsgUrl_)
+        logMessage(logMsg, this.logMsgUrl_);
+    }
     this.notifyError_(reportedError);
   } else {
     if (this.logMsgUrl_ && opt_source) {
diff --git a/chrome/browser/resources/cryptotoken/singlesigner.js b/chrome/browser/resources/cryptotoken/singlesigner.js
index f9e46f0..e39e4008 100644
--- a/chrome/browser/resources/cryptotoken/singlesigner.js
+++ b/chrome/browser/resources/cryptotoken/singlesigner.js
@@ -13,9 +13,19 @@
 
 /**
  * @typedef {{
+ *   challengeHash: Array<number>,
+ *   appIdHash: Array<number>,
+ *   keyHandle: Array<number>,
+ *   version: (string|undefined)
+ * }}
+ */
+var DecodedSignHelperChallenge;
+
+/**
+ * @typedef {{
  *   code: number,
  *   gnubby: (Gnubby|undefined),
- *   challenge: (SignHelperChallenge|undefined),
+ *   challenge: (DecodedSignHelperChallenge|undefined),
  *   info: (ArrayBuffer|undefined)
  * }}
  */
@@ -65,14 +75,14 @@
   /** @private {string|undefined} */
   this.logMsgUrl_ = opt_logMsgUrl;
 
-  /** @private {!Array<!SignHelperChallenge>} */
+  /** @private {!Array<!DecodedSignHelperChallenge>} */
   this.challenges_ = [];
   /** @private {number} */
   this.challengeIndex_ = 0;
   /** @private {boolean} */
   this.challengesSet_ = false;
 
-  /** @private {!Object<string, number>} */
+  /** @private {!Object<Array<number>, number>} */
   this.cachedError_ = [];
 
   /** @private {(function()|undefined)} */
@@ -132,7 +142,7 @@
 
 /**
  * Begins signing the given challenges.
- * @param {Array<SignHelperChallenge>} challenges The challenges to sign.
+ * @param {Array<DecodedSignHelperChallenge>} challenges The challenges to sign.
  * @return {boolean} Whether the challenges were accepted.
  */
 SingleGnubbySigner.prototype.doSign = function(challenges) {
@@ -481,7 +491,7 @@
 /**
  * Switches to the success state, and notifies caller.
  * @param {number} code Status code
- * @param {SignHelperChallenge=} opt_challenge The challenge signed
+ * @param {DecodedSignHelperChallenge=} opt_challenge The challenge signed
  * @param {ArrayBuffer=} opt_info Optional result data
  * @private
  */
diff --git a/chrome/browser/resources/cryptotoken/usbenrollhandler.js b/chrome/browser/resources/cryptotoken/usbenrollhandler.js
index e9bdcc11..2d53b07 100644
--- a/chrome/browser/resources/cryptotoken/usbenrollhandler.js
+++ b/chrome/browser/resources/cryptotoken/usbenrollhandler.js
@@ -284,8 +284,16 @@
       break;
 
     case DeviceStatusCodes.OK_STATUS:
-      var info = B64_encode(new Uint8Array(infoArray || []));
-      this.notifySuccess_(version, info);
+      var appIdHash = this.request_.enrollChallenges[0].appIdHash;
+      DEVICE_FACTORY_REGISTRY.getGnubbyFactory().postEnrollAction(
+          gnubby, appIdHash, (rc) => {
+            if (rc == DeviceStatusCodes.OK_STATUS) {
+              var info = B64_encode(new Uint8Array(infoArray || []));
+              this.notifySuccess_(version, info);
+            } else {
+              this.notifyError_(rc);
+            }
+          });
       break;
 
     default:
diff --git a/chrome/browser/resources/cryptotoken/usbgnubbydevice.js b/chrome/browser/resources/cryptotoken/usbgnubbydevice.js
index aeaaf85d..ec100c2 100644
--- a/chrome/browser/resources/cryptotoken/usbgnubbydevice.js
+++ b/chrome/browser/resources/cryptotoken/usbgnubbydevice.js
@@ -198,7 +198,7 @@
       }, 0);
     } else {
       console.log(UTIL_fmt('no x.data!'));
-      console.log(x);
+      console.log(UTIL_fmt(JSON.stringify(x)));
       window.setTimeout(function() {
         self.destroy();
       }, 0);
diff --git a/chrome/browser/resources/cryptotoken/usbgnubbyfactory.js b/chrome/browser/resources/cryptotoken/usbgnubbyfactory.js
index dd35395..f797c6e 100644
--- a/chrome/browser/resources/cryptotoken/usbgnubbyfactory.js
+++ b/chrome/browser/resources/cryptotoken/usbgnubbyfactory.js
@@ -65,3 +65,15 @@
     gnubby, appIdHash, cb) {
   cb(DeviceStatusCodes.OK_STATUS, gnubby);
 };
+
+/**
+ * No-op post enroll action.
+ * @param {Gnubby} gnubby The just-enrolled gnubby.
+ * @param {string} appIdHash The base64-encoded hash of the app id for which
+ *     the gnubby was enrolled.
+ * @param {FactoryOpenCallback} cb Called with the result of the action.
+ *     (A non-zero status indicates failure.)
+ */
+UsbGnubbyFactory.prototype.postEnrollAction = function(gnubby, appIdHash, cb) {
+  cb(DeviceStatusCodes.OK_STATUS, gnubby);
+};
diff --git a/content/test/gpu/gpu_tests/webgl2_conformance_expectations.py b/content/test/gpu/gpu_tests/webgl2_conformance_expectations.py
index fac743cf..1fc58af 100644
--- a/content/test/gpu/gpu_tests/webgl2_conformance_expectations.py
+++ b/content/test/gpu/gpu_tests/webgl2_conformance_expectations.py
@@ -175,6 +175,8 @@
     self.Fail('conformance2/textures/canvas_sub_rectangle/' +
         'tex-2d-r16f-red-float.html',
         ['win', 'nvidia', 'opengl'], bug=786716)
+    self.Fail('conformance2/rendering/instanced-rendering-bug.html',
+        ['win', 'nvidia', 'opengl'], bug=791289)
 
     # Win / AMD
     self.Fail('conformance2/rendering/blitframebuffer-stencil-only.html',