lorgnette: Enforce maximum scan buffer size

When starting to return a page, a scanner can claim arbitrary
dimensions.  Lorgnette reads line by line through a modest-sized buffer,
but this could still result in creating an output PNG of unbounded size.
To avoid this, ensure that the scanner's claimed parameters are
self-consistent values that don't result in more than 400MB of raw data.
This limit is enough to scan:

  * An 11x17 ledger page at 600 dpi in 16-bit color.
  * An 8.5x40 banner from the ADF at 600 dpi in 16-bit color.
  * An 8.5x11 letter page at 1200 dpi in 8-bit color.

Since the frontend currently limits requests to 600 dpi and 8-bit
color, this limit should provide a safety net without hampering any
realistic consumer-level scanner.

BUG=b:175588023
TEST=New unit test

Change-Id: Ie0944a1865e176beec38415be033e510d4e9e526
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/2594491
Commit-Queue: Benjamin Gordon <bmgordon@chromium.org>
Tested-by: Benjamin Gordon <bmgordon@chromium.org>
Reviewed-by: Allen Webb <allenwebb@google.com>
Reviewed-by: Jesse Schettler <jschettler@chromium.org>
diff --git a/lorgnette/manager.cc b/lorgnette/manager.cc
index 3dab5f1..058c320 100644
--- a/lorgnette/manager.cc
+++ b/lorgnette/manager.cc
@@ -4,6 +4,7 @@
 
 #include "lorgnette/manager.h"
 
+#include <inttypes.h>
 #include <setjmp.h>
 
 #include <algorithm>
@@ -40,6 +41,19 @@
     base::TimeDelta::FromMilliseconds(20);
 constexpr size_t kUUIDStringLength = 37;
 
+// The maximum memory size allowed to be allocated for an image.  At the current
+// maximum resolution and color depth that the frontend will request, this gives
+// 388 sq in, which is more than enough for an 11x17 ledger page or an 8.5x40
+// ADF scan.  This limit will need to be reconsidered if we want to enable 1200
+// dpi scanning.
+constexpr size_t kMaximumImageSize = 400 * 1024 * 1024;
+
+// The default libpng config limits images to 1 million pixels in width and
+// height.  Update these constants to match if you add a call to
+// png_set_user_limits that changes the defaults.
+constexpr size_t kMaximiumImageWidth = 1000000;
+constexpr size_t kMaximiumImageHeight = 1000000;
+
 std::string SerializeError(const brillo::ErrorPtr& error_ptr) {
   std::string message;
   const brillo::Error* error = error_ptr.get();
@@ -82,6 +96,50 @@
                          "Cannot scan an image with 0 lines");
     return false;
   }
+
+  // PNG IHDR allows a max height of 2^31, but libpng imposes a default limit of
+  // 1 million.  We'll impose the same limit here, since that represents 800+
+  // inches at 1200 dpi.
+  if (params.lines > kMaximiumImageHeight) {
+    brillo::Error::AddToPrintf(
+        error, FROM_HERE, kDbusDomain, kManagerServiceError,
+        "Cannot scan an image with invalid height (%d)", params.lines);
+    return false;
+  }
+
+  // PNG IHDR allows a max width of 2^31 and requires a non-zero width.
+  // We follow the default libpng limit of 1 million rather than the true max
+  // because no real scanner can produce an image that wide.
+  if (params.pixels_per_line <= 0 ||
+      params.pixels_per_line > kMaximiumImageWidth) {
+    brillo::Error::AddToPrintf(
+        error, FROM_HERE, kDbusDomain, kManagerServiceError,
+        "Cannot scan an image with invalid width (%d)", params.pixels_per_line);
+    return false;
+  }
+
+  // Make sure bytes_per_line is large enough to be plausible for
+  // pixels_per_line.  It is allowed to be bigger in case the device pads up to
+  // a multiple of some internal size.
+  size_t colors_per_pixel = params.format == kRGB ? 3 : 1;
+  uint64_t min_bytes_per_line =
+      (params.pixels_per_line * params.depth * colors_per_pixel + 7) / 8;
+  if (params.bytes_per_line < min_bytes_per_line) {
+    brillo::Error::AddToPrintf(
+        error, FROM_HERE, kDbusDomain, kManagerServiceError,
+        "bytes_per_line (%d) is too small to hold %d pixels with depth %d",
+        params.bytes_per_line, params.pixels_per_line, params.depth);
+    return false;
+  }
+
+  uint64_t needed = params.lines * params.bytes_per_line;
+  if (needed > kMaximumImageSize) {
+    brillo::Error::AddToPrintf(
+        error, FROM_HERE, kDbusDomain, kManagerServiceError,
+        "Needed scan buffer size of %" PRIu64 " is too large", needed);
+    return false;
+  }
+
   return true;
 }
 
diff --git a/lorgnette/manager_test.cc b/lorgnette/manager_test.cc
index a8fb262..b1813f6 100644
--- a/lorgnette/manager_test.cc
+++ b/lorgnette/manager_test.cc
@@ -31,6 +31,7 @@
 #include "lorgnette/test_util.h"
 
 using brillo::dbus_utils::MockDBusMethodResponse;
+using ::testing::ContainsRegex;
 using ::testing::ElementsAre;
 
 namespace lorgnette {
@@ -531,6 +532,153 @@
   ValidateSignals(signals_, response.scan_uuid());
 }
 
+TEST_F(ManagerTest, GetNextImageNegativeWidth) {
+  ScanParameters parameters;
+  parameters.format = kRGB;
+  parameters.bytes_per_line = 100;
+  parameters.pixels_per_line = -1;
+  parameters.lines = 11;
+  parameters.depth = 16;
+  SetUpTestDevice("TestDevice", {base::FilePath("./test_images/color.pnm")},
+                  parameters);
+
+  ExpectScanRequest(kOtherBackend);
+  ExpectScanFailure(kOtherBackend);
+  StartScanResponse response = StartScan("TestDevice", MODE_COLOR, "Flatbed");
+
+  EXPECT_EQ(response.state(), SCAN_STATE_IN_PROGRESS);
+  EXPECT_EQ(response.failure_reason(), "");
+  EXPECT_NE(response.scan_uuid(), "");
+
+  GetNextImageResponse get_next_image_response =
+      GetNextImage(response.scan_uuid(), scan_fd_);
+  EXPECT_TRUE(get_next_image_response.success());
+  EXPECT_EQ(get_next_image_response.failure_reason(), "");
+
+  EXPECT_EQ(signals_.size(), 1);
+  EXPECT_EQ(signals_[0].scan_uuid(), response.scan_uuid());
+  EXPECT_EQ(signals_[0].state(), SCAN_STATE_FAILED);
+  EXPECT_THAT(signals_[0].failure_reason(), ContainsRegex("invalid width"));
+}
+
+TEST_F(ManagerTest, GetNextImageExcessWidth) {
+  ScanParameters parameters;
+  parameters.format = kRGB;
+  parameters.bytes_per_line = 3000003;
+  parameters.pixels_per_line = 1000001;
+  parameters.lines = 100;
+  parameters.depth = 8;
+  SetUpTestDevice("TestDevice", {base::FilePath("./test_images/color.pnm")},
+                  parameters);
+
+  ExpectScanRequest(kOtherBackend);
+  ExpectScanFailure(kOtherBackend);
+  StartScanResponse response = StartScan("TestDevice", MODE_COLOR, "Flatbed");
+
+  EXPECT_EQ(response.state(), SCAN_STATE_IN_PROGRESS);
+  EXPECT_EQ(response.failure_reason(), "");
+  EXPECT_NE(response.scan_uuid(), "");
+
+  GetNextImageResponse get_next_image_response =
+      GetNextImage(response.scan_uuid(), scan_fd_);
+  EXPECT_TRUE(get_next_image_response.success());
+  EXPECT_EQ(get_next_image_response.failure_reason(), "");
+
+  EXPECT_EQ(signals_.size(), 1);
+  EXPECT_EQ(signals_[0].scan_uuid(), response.scan_uuid());
+  EXPECT_EQ(signals_[0].state(), SCAN_STATE_FAILED);
+  EXPECT_THAT(signals_[0].failure_reason(), ContainsRegex("invalid width"));
+}
+
+TEST_F(ManagerTest, GetNextImageInvalidHeight) {
+  ScanParameters parameters;
+  parameters.format = kRGB;
+  parameters.bytes_per_line = 0x40000000 + (0x10 * 0x08);
+  parameters.pixels_per_line = 0x10;
+  parameters.lines = 0x02000000;
+  parameters.depth = 8;
+  SetUpTestDevice("TestDevice", {base::FilePath("./test_images/color.pnm")},
+                  parameters);
+
+  ExpectScanRequest(kOtherBackend);
+  ExpectScanFailure(kOtherBackend);
+  StartScanResponse response = StartScan("TestDevice", MODE_COLOR, "Flatbed");
+
+  EXPECT_EQ(response.state(), SCAN_STATE_IN_PROGRESS);
+  EXPECT_EQ(response.failure_reason(), "");
+  EXPECT_NE(response.scan_uuid(), "");
+
+  GetNextImageResponse get_next_image_response =
+      GetNextImage(response.scan_uuid(), scan_fd_);
+  EXPECT_TRUE(get_next_image_response.success());
+  EXPECT_EQ(get_next_image_response.failure_reason(), "");
+
+  EXPECT_EQ(signals_.size(), 1);
+  EXPECT_EQ(signals_[0].scan_uuid(), response.scan_uuid());
+  EXPECT_EQ(signals_[0].state(), SCAN_STATE_FAILED);
+  EXPECT_THAT(signals_[0].failure_reason(), ContainsRegex("invalid height"));
+}
+
+TEST_F(ManagerTest, GetNextImageMismatchedSizes) {
+  ScanParameters parameters;
+  parameters.format = kRGB;
+  parameters.bytes_per_line = 8.5 * 1200;
+  parameters.pixels_per_line = 8.5 * 1200;
+  parameters.lines = 11 * 1200;
+  parameters.depth = 8;
+  SetUpTestDevice("TestDevice", {base::FilePath("./test_images/color.pnm")},
+                  parameters);
+
+  ExpectScanRequest(kOtherBackend);
+  ExpectScanFailure(kOtherBackend);
+  StartScanResponse response = StartScan("TestDevice", MODE_COLOR, "Flatbed");
+
+  EXPECT_EQ(response.state(), SCAN_STATE_IN_PROGRESS);
+  EXPECT_EQ(response.failure_reason(), "");
+  EXPECT_NE(response.scan_uuid(), "");
+
+  GetNextImageResponse get_next_image_response =
+      GetNextImage(response.scan_uuid(), scan_fd_);
+  EXPECT_TRUE(get_next_image_response.success());
+  EXPECT_EQ(get_next_image_response.failure_reason(), "");
+
+  EXPECT_EQ(signals_.size(), 1);
+  EXPECT_EQ(signals_[0].scan_uuid(), response.scan_uuid());
+  EXPECT_EQ(signals_[0].state(), SCAN_STATE_FAILED);
+  EXPECT_THAT(signals_[0].failure_reason(),
+              ContainsRegex("bytes_per_line.*too small"));
+}
+
+TEST_F(ManagerTest, GetNextImageTooLarge) {
+  ScanParameters parameters;
+  parameters.format = kRGB;
+  parameters.bytes_per_line = 8.5 * 1200 * 6;
+  parameters.pixels_per_line = 8.5 * 1200;
+  parameters.lines = 11 * 1200;
+  parameters.depth = 16;
+  SetUpTestDevice("TestDevice", {base::FilePath("./test_images/color.pnm")},
+                  parameters);
+
+  ExpectScanRequest(kOtherBackend);
+  ExpectScanFailure(kOtherBackend);
+  StartScanResponse response = StartScan("TestDevice", MODE_COLOR, "Flatbed");
+
+  EXPECT_EQ(response.state(), SCAN_STATE_IN_PROGRESS);
+  EXPECT_EQ(response.failure_reason(), "");
+  EXPECT_NE(response.scan_uuid(), "");
+
+  GetNextImageResponse get_next_image_response =
+      GetNextImage(response.scan_uuid(), scan_fd_);
+  EXPECT_TRUE(get_next_image_response.success());
+  EXPECT_EQ(get_next_image_response.failure_reason(), "");
+
+  EXPECT_EQ(signals_.size(), 1);
+  EXPECT_EQ(signals_[0].scan_uuid(), response.scan_uuid());
+  EXPECT_EQ(signals_[0].state(), SCAN_STATE_FAILED);
+  EXPECT_THAT(signals_[0].failure_reason(),
+              ContainsRegex("scan buffer.*too large"));
+}
+
 TEST_F(ManagerTest, RemoveDupNoRepeats) {
   std::vector<ScannerInfo> scanners_empty, scanners_present, sane_scanners,
       expected_present;