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;