Bug 1468556 - Protect against overlapping files in libmar; r=mhowell

Disallows files from referencing the same bytes in the content blocks of a MAR
file by storing a list of structs containing a file's byte offsets and lengths.
A list was chosen since the cap of 256 files wouldn't produce considerable
overhead when extracting/reading/searching/etc through the archive.

Removing the ability for a MAR file to reference the same content block
repeatedly seems like a better solution than what was suggested in the BLRG
report. (limiting the number of files or checking for overly large
decompressed files)

Allows us to prohibit this type of file bomb while only losing an attribute
of the MAR file format that wasn't being leveraged. The fix is applied in
mar_enum_items and mar_find_item so that the manifest the updater uses is
equally safeguarded as the mar host tool.

Differential Revision: https://phabricator.services.mozilla.com/D11706

--HG--
extra : moz-landing-system : lando
This commit is contained in:
June Wilde 2018-11-26 17:25:24 +00:00
parent 6a9118931c
commit 3dba77a779
12 changed files with 277 additions and 89 deletions

View File

@ -27,28 +27,45 @@ static_assert(MAX_SIGNATURES <= 9, "too many signatures");
MOZ_STATIC_ASSERT(MAX_SIGNATURES <= 9, "too many signatures");
#endif
struct ProductInformationBlock {
const char *MARChannelID;
const char *productVersion;
struct ProductInformationBlock
{
const char* MARChannelID;
const char* productVersion;
};
/**
* The MAR item data structure.
*/
typedef struct MarItem_ {
struct MarItem_ *next; /* private field */
typedef struct MarItem_
{
struct MarItem_* next; /* private field */
uint32_t offset; /* offset into archive */
uint32_t length; /* length of data in bytes */
uint32_t flags; /* contains file mode bits */
char name[1]; /* file path */
} MarItem;
/**
* File offset and length for tracking access of byte indexes
*/
typedef struct SeenIndex_
{
struct SeenIndex_* next; /* private field */
uint32_t offset; /* offset into archive */
uint32_t length; /* length of the data in bytes */
} SeenIndex;
#define TABLESIZE 256
struct MarFile_ {
FILE *fp;
MarItem *item_table[TABLESIZE];
int item_table_is_valid;
/**
* Mozilla ARchive (MAR) file data structure
*/
struct MarFile_
{
FILE* fp; /* file pointer to the archive */
MarItem* item_table[TABLESIZE]; /* hash table of files in the archive */
SeenIndex* index_list; /* file indexes processed */
int item_table_is_valid; /* header and index validation flag */
};
typedef struct MarFile_ MarFile;
@ -60,7 +77,7 @@ typedef struct MarFile_ MarFile;
* @param data The data parameter passed by the caller of mar_enum_items.
* @return A non-zero value to stop enumerating.
*/
typedef int (* MarItemCallback)(MarFile *mar, const MarItem *item, void *data);
typedef int (*MarItemCallback)(MarFile* mar, const MarItem* item, void* data);
/**
* Open a MAR file for reading.
@ -68,7 +85,8 @@ typedef int (* MarItemCallback)(MarFile *mar, const MarItem *item, void *data);
* be compatible with fopen.
* @return NULL if an error occurs.
*/
MarFile *mar_open(const char *path);
MarFile*
mar_open(const char* path);
#ifdef XP_WIN
MarFile *mar_wopen(const wchar_t *path);
@ -78,7 +96,8 @@ MarFile *mar_wopen(const wchar_t *path);
* Close a MAR file that was opened using mar_open.
* @param mar The MarFile object to close.
*/
void mar_close(MarFile *mar);
void
mar_close(MarFile* mar);
/**
* Find an item in the MAR file by name.
@ -86,7 +105,8 @@ void mar_close(MarFile *mar);
* @param item The name of the item to query.
* @return A const reference to a MAR item or NULL if not found.
*/
const MarItem *mar_find_item(MarFile *mar, const char *item);
const MarItem*
mar_find_item(MarFile* mar, const char* item);
/**
* Enumerate all MAR items via callback function.
@ -97,7 +117,8 @@ const MarItem *mar_find_item(MarFile *mar, const char *item);
* @return 0 if the enumeration ran to completion. Otherwise, any
* non-zero return value from the callback is returned.
*/
int mar_enum_items(MarFile *mar, MarItemCallback callback, void *data);
int
mar_enum_items(MarFile* mar, MarItemCallback callback, void* data);
/**
* Read from MAR item at given offset up to bufsize bytes.
@ -109,8 +130,12 @@ int mar_enum_items(MarFile *mar, MarItemCallback callback, void *data);
* @return The number of bytes written or a negative value if an
* error occurs.
*/
int mar_read(MarFile *mar, const MarItem *item, int offset, uint8_t *buf,
int bufsize);
int
mar_read(MarFile* mar,
const MarItem* item,
int offset,
uint8_t* buf,
int bufsize);
/**
* Create a MAR file from a set of files.
@ -122,10 +147,11 @@ int mar_read(MarFile *mar, const MarItem *item, int offset, uint8_t *buf,
* @param infoBlock The information to store in the product information block.
* @return A non-zero value if an error occurs.
*/
int mar_create(const char *dest,
int numfiles,
char **files,
struct ProductInformationBlock *infoBlock);
int
mar_create(const char* dest,
int numfiles,
char** files,
struct ProductInformationBlock* infoBlock);
/**
* Extract a MAR file to the current working directory.
@ -133,7 +159,8 @@ int mar_create(const char *dest,
* compatible with fopen.
* @return A non-zero value if an error occurs.
*/
int mar_extract(const char *path);
int
mar_extract(const char* path);
#define MAR_MAX_CERT_SIZE (16*1024) // Way larger than necessary
@ -150,10 +177,11 @@ int mar_extract(const char *path);
*
* @return 0 on success, -1 on error
*/
int mar_read_entire_file(const char * filePath,
uint32_t maxSize,
/*out*/ const uint8_t * *data,
/*out*/ uint32_t *size);
int
mar_read_entire_file(const char* filePath,
uint32_t maxSize,
/*out*/ const uint8_t** data,
/*out*/ uint32_t* size);
/**
* Verifies a MAR file by verifying each signature with the corresponding
@ -175,10 +203,11 @@ int mar_read_entire_file(const char * filePath,
* a negative number if there was an error
* a positive number if the signature does not verify
*/
int mar_verify_signatures(MarFile *mar,
const uint8_t * const *certData,
const uint32_t *certDataSizes,
uint32_t certCount);
int
mar_verify_signatures(MarFile* mar,
const uint8_t* const* certData,
const uint32_t* certDataSizes,
uint32_t certCount);
/**
* Reads the product info block from the MAR file's additional block section.
@ -189,8 +218,8 @@ int mar_verify_signatures(MarFile *mar,
* @return 0 on success, -1 on failure
*/
int
mar_read_product_info_block(MarFile *mar,
struct ProductInformationBlock *infoBlock);
mar_read_product_info_block(MarFile* mar,
struct ProductInformationBlock* infoBlock);
#ifdef __cplusplus
}

View File

@ -19,12 +19,20 @@
sizeof(additionalBlockSize) and sizeof(additionalBlockID) */
#define MAXADDITIONALBLOCKSIZE 96
static uint32_t mar_hash_name(const char *name) {
static uint32_t
mar_hash_name(const char* name)
{
return CityHash64(name, strlen(name)) % TABLESIZE;
}
static int mar_insert_item(MarFile *mar, const char *name, int namelen,
uint32_t offset, uint32_t length, uint32_t flags) {
static int
mar_insert_item(MarFile* mar,
const char* name,
int namelen,
uint32_t offset,
uint32_t length,
uint32_t flags)
{
MarItem *item, *root;
uint32_t hash;
@ -51,7 +59,9 @@ static int mar_insert_item(MarFile *mar, const char *name, int namelen,
return 0;
}
static int mar_consume_index(MarFile *mar, char **buf, const char *buf_end) {
static int
mar_consume_index(MarFile* mar, char** buf, const char* buf_end)
{
/*
* Each item has the following structure:
* uint32_t offset (network byte order)
@ -103,7 +113,9 @@ static int mar_consume_index(MarFile *mar, char **buf, const char *buf_end) {
return mar_insert_item(mar, name, namelen, offset, length, flags);
}
static int mar_read_index(MarFile *mar) {
static int
mar_read_index(MarFile* mar)
{
char id[MAR_ID_SIZE], *buf, *bufptr, *bufend;
uint32_t offset_to_index, size_of_index;
@ -140,15 +152,73 @@ static int mar_read_index(MarFile *mar) {
return (bufptr == bufend) ? 0 : -1;
}
/**
* Adds an offset and length to the MarFile's index_list
* @param mar The MarFile that owns this offset length pair
* @param offset The byte offset in the archive to be marked as processed
* @param length The length corresponding to this byte offset
* @return int 1 on success, 0 if offset has been previously processed
* -1 if unable to allocate space for the SeenIndexes
*/
static int
mar_insert_offset(MarFile* mar, uint32_t offset, uint32_t length)
{
/* Ignore files with no length */
if (length == 0) {
return 1;
}
SeenIndex* index = (SeenIndex*)malloc(sizeof(SeenIndex));
if (!index) {
return -1;
}
index->next = NULL;
index->offset = offset;
index->length = length;
uint32_t index_end = index->offset + index->length - 1;
/* If this is our first index store it at the front */
if (mar->index_list == NULL) {
mar->index_list = index;
return 1;
}
/* Search for matching indexes in the list of those previously visited */
SeenIndex* previous;
SeenIndex* current = mar->index_list;
while (current != NULL) {
uint32_t current_end = current->offset + current->length - 1;
/* If index has collided with the front or end of current or if current has
collided with the front or end of index return false */
if ((index->offset >= current->offset && index->offset <= current_end) ||
(index_end >= current->offset && index_end <= current_end) ||
(current->offset >= index->offset && current->offset <= index_end) ||
(current_end >= index->offset && current_end <= index_end)) {
free(index);
return 0;
}
/* else move to the next in the list */
previous = current;
current = current->next;
}
/* These indexes are valid, track them */
previous->next = index;
return 1;
}
/**
* Internal shared code for mar_open and mar_wopen.
* On failure, will fclose(fp).
*/
static MarFile *mar_fpopen(FILE *fp)
static MarFile*
mar_fpopen(FILE* fp)
{
MarFile *mar;
MarFile* mar;
mar = (MarFile *) malloc(sizeof(*mar));
mar = (MarFile*)malloc(sizeof(*mar));
if (!mar) {
fclose(fp);
return NULL;
@ -157,11 +227,14 @@ static MarFile *mar_fpopen(FILE *fp)
mar->fp = fp;
mar->item_table_is_valid = 0;
memset(mar->item_table, 0, sizeof(mar->item_table));
mar->index_list = NULL;
return mar;
}
MarFile *mar_open(const char *path) {
MarFile*
mar_open(const char* path)
{
FILE *fp;
fp = fopen(path, "rb");
@ -175,7 +248,9 @@ MarFile *mar_open(const char *path) {
}
#ifdef XP_WIN
MarFile *mar_wopen(const wchar_t *path) {
MarFile*
mar_wopen(const wchar_t* path)
{
FILE *fp;
_wfopen_s(&fp, path, L"rb");
@ -189,8 +264,11 @@ MarFile *mar_wopen(const wchar_t *path) {
}
#endif
void mar_close(MarFile *mar) {
MarItem *item;
void
mar_close(MarFile* mar)
{
MarItem* item;
SeenIndex* index;
int i;
fclose(mar->fp);
@ -198,12 +276,18 @@ void mar_close(MarFile *mar) {
for (i = 0; i < TABLESIZE; ++i) {
item = mar->item_table[i];
while (item) {
MarItem *temp = item;
MarItem* temp = item;
item = item->next;
free(temp);
}
}
while (mar->index_list != NULL) {
index = mar->index_list;
mar->index_list = index->next;
free(index);
}
free(mar);
}
@ -225,12 +309,13 @@ void mar_close(MarFile *mar) {
* hasAdditionalBlocks is not equal to 0.
* @return 0 on success and non-zero on failure.
*/
int get_mar_file_info_fp(FILE *fp,
int *hasSignatureBlock,
uint32_t *numSignatures,
int *hasAdditionalBlocks,
uint32_t *offsetAdditionalBlocks,
uint32_t *numAdditionalBlocks)
int
get_mar_file_info_fp(FILE* fp,
int* hasSignatureBlock,
uint32_t* numSignatures,
int* hasAdditionalBlocks,
uint32_t* offsetAdditionalBlocks,
uint32_t* numAdditionalBlocks)
{
uint32_t offsetToIndex, offsetToContent, signatureCount, signatureLen, i;
@ -363,8 +448,7 @@ int get_mar_file_info_fp(FILE *fp,
* @return 0 on success, -1 on failure
*/
int
read_product_info_block(char *path,
struct ProductInformationBlock *infoBlock)
read_product_info_block(char* path, struct ProductInformationBlock* infoBlock)
{
int rv;
MarFile mar;
@ -388,8 +472,8 @@ read_product_info_block(char *path,
* @return 0 on success, -1 on failure
*/
int
mar_read_product_info_block(MarFile *mar,
struct ProductInformationBlock *infoBlock)
mar_read_product_info_block(MarFile* mar,
struct ProductInformationBlock* infoBlock)
{
uint32_t offsetAdditionalBlocks, numAdditionalBlocks,
additionalBlockSize, additionalBlockID;
@ -476,9 +560,11 @@ mar_read_product_info_block(MarFile *mar,
return -1;
}
const MarItem *mar_find_item(MarFile *mar, const char *name) {
const MarItem*
mar_find_item(MarFile* mar, const char* name)
{
uint32_t hash;
const MarItem *item;
const MarItem* item;
if (!mar->item_table_is_valid) {
if (mar_read_index(mar)) {
@ -491,15 +577,24 @@ const MarItem *mar_find_item(MarFile *mar, const char *name) {
hash = mar_hash_name(name);
item = mar->item_table[hash];
while (item && strcmp(item->name, name) != 0)
while (item && strcmp(item->name, name) != 0) {
item = item->next;
}
return item;
/* If this is the first time seeing this item's indexes, return it */
if (mar_insert_offset(mar, item->offset, item->length) == 1) {
return item;
} else {
fprintf(stderr, "ERROR: file content collision in mar_find_item()\n");
return NULL;
}
}
int mar_enum_items(MarFile *mar, MarItemCallback callback, void *closure) {
MarItem *item;
int i;
int
mar_enum_items(MarFile* mar, MarItemCallback callback, void* closure)
{
MarItem* item;
int i, rv;
if (!mar->item_table_is_valid) {
if (mar_read_index(mar)) {
@ -512,9 +607,16 @@ int mar_enum_items(MarFile *mar, MarItemCallback callback, void *closure) {
for (i = 0; i < TABLESIZE; ++i) {
item = mar->item_table[i];
while (item) {
int rv = callback(mar, item, closure);
if (rv)
return rv;
/* if this is the first time seeing this item's indexes, process it */
if (mar_insert_offset(mar, item->offset, item->length) == 1) {
rv = callback(mar, item, closure);
if (rv) {
return rv;
}
} else {
fprintf(stderr, "ERROR: file content collision in mar_enum_items()\n");
return 1;
}
item = item->next;
}
}
@ -522,8 +624,13 @@ int mar_enum_items(MarFile *mar, MarItemCallback callback, void *closure) {
return 0;
}
int mar_read(MarFile *mar, const MarItem *item, int offset, uint8_t *buf,
int bufsize) {
int
mar_read(MarFile* mar,
const MarItem* item,
int offset,
uint8_t* buf,
int bufsize)
{
int nr;
if (offset == (int) item->length)
@ -559,12 +666,13 @@ int mar_read(MarFile *mar, const MarItem *item, int offset, uint8_t *buf,
* has_additional_blocks is not equal to 0.
* @return 0 on success and non-zero on failure.
*/
int get_mar_file_info(const char *path,
int *hasSignatureBlock,
uint32_t *numSignatures,
int *hasAdditionalBlocks,
uint32_t *offsetAdditionalBlocks,
uint32_t *numAdditionalBlocks)
int
get_mar_file_info(const char* path,
int* hasSignatureBlock,
uint32_t* numSignatures,
int* hasAdditionalBlocks,
uint32_t* offsetAdditionalBlocks,
uint32_t* numAdditionalBlocks)
{
int rv;
FILE *fp = fopen(path, "rb");

View File

@ -1,7 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
'use strict';
"use strict";
const BIN_SUFFIX = mozinfo.bin_suffix;
const tempDir = do_get_tempdir();
@ -17,7 +17,7 @@ function compareBinaryData(arr1, arr2) {
Assert.equal(arr1.length, arr2.length);
for (let i = 0; i < arr1.length; i++) {
if (arr1[i] != arr2[i]) {
throw "Data differs at index " + i +
throw "Data differs at index " + i +
", arr1: " + arr1[i] + ", arr2: " + arr2[i];
}
}
@ -113,7 +113,7 @@ function createMAR(outMAR, dataDir, files) {
"-V", "13.0a1", "-c", outMAR.path];
args = args.concat(files);
info('Running: ' + signmarBin.path + " " + args.join(" "));
info("Running: " + signmarBin.path + " " + args.join(" "));
process.init(signmarBin);
process.run(true, args, args.length);
@ -140,13 +140,12 @@ function extractMAR(mar, dataDir) {
Assert.ok(signmarBin.exists());
Assert.ok(signmarBin.isExecutable());
// Setup the command line arguments to create the MAR.
// Setup the command line arguments to extract the MAR.
let args = ["-C", dataDir.path, "-x", mar.path];
info('Running: ' + signmarBin.path + " " + args.join(" "));
info("Running: " + signmarBin.path + " " + args.join(" "));
process.init(signmarBin);
process.run(true, args, args.length);
// Verify signmar returned 0 for success.
Assert.equal(process.exitValue, 0);
return process.exitValue;
}

View File

@ -9,7 +9,7 @@ function run_test() {
* @param marFileName The name of the MAR file to extract
* @param files The files that the extracted MAR should contain
*/
function run_one_test(marFileName, files) {
function extract_and_compare(marFileName, files) {
// Get the MAR file that we will be extracting
let mar = do_get_file("data/" + marFileName);
@ -31,8 +31,8 @@ function run_test() {
refFiles.push(do_get_file("data/" + files[i]));
}
// Extract the MAR contents into the ./out dir.
extractMAR(mar, outDir);
// Extract the MAR contents to ./out dir and verify 0 for success.
Assert.equal(extractMAR(mar, outDir), 0);
// Compare to make sure the extracted files are the same.
for (let i = 0; i < files.length; i++) {
@ -43,40 +43,92 @@ function run_test() {
}
}
/**
* Attempts to extract a MAR and expects a failure
*
* @param marFileName The name of the MAR file to extract
*/
function extract_and_fail(marFileName) {
// Get the MAR file that we will be extracting
let mar = do_get_file("data/" + marFileName);
// Get the path that we will extract to
let outDir = tempDir.clone();
outDir.append("out");
Assert.ok(!outDir.exists());
outDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o777);
// Extract the MAR contents to ./out dir and verify -1 (255 from the
// nsIprocess) for failure
Assert.equal(extractMAR(mar, outDir), 1);
}
// Define the unit tests to run.
let tests = {
// Test extracting a MAR file with a 0 byte file.
test_zero_sized: function _test_zero_sized() {
return run_one_test("0_sized.mar", ["0_sized_file"]);
return extract_and_compare("0_sized.mar", ["0_sized_file"]);
},
// Test extracting a MAR file with a 1 byte file.
test_one_byte: function _test_one_byte() {
return run_one_test("1_byte.mar", ["1_byte_file"]);
return extract_and_compare("1_byte.mar", ["1_byte_file"]);
},
// Test extracting a MAR file with binary data.
test_binary_data: function _test_binary_data() {
return run_one_test("binary_data.mar", ["binary_data_file"]);
return extract_and_compare("binary_data.mar", ["binary_data_file"]);
},
// Test extracting a MAR without a product information block (PIB) which
// contains binary data.
test_no_pib: function _test_no_pib() {
return run_one_test("no_pib.mar", ["binary_data_file"]);
return extract_and_compare("no_pib.mar", ["binary_data_file"]);
},
// Test extracting a MAR without a product information block (PIB) that is
// signed and which contains binary data.
test_no_pib_signed: function _test_no_pib_signed() {
return run_one_test("signed_no_pib.mar", ["binary_data_file"]);
return extract_and_compare("signed_no_pib.mar", ["binary_data_file"]);
},
// Test extracting a MAR with a product information block (PIB) that is
// signed and which contains binary data.
test_pib_signed: function _test_pib_signed() {
return run_one_test("signed_pib.mar", ["binary_data_file"]);
return extract_and_compare("signed_pib.mar", ["binary_data_file"]);
},
// Test extracting a MAR file with multiple files inside of it.
test_multiple_file: function _test_multiple_file() {
return run_one_test("multiple_file.mar",
return extract_and_compare("multiple_file.mar",
["0_sized_file", "1_byte_file", "binary_data_file"]);
},
// Test collision detection where file A + B are the same offset
test_collision_same_offset: function test_collision_same_offset() {
return extract_and_fail("manipulated_same_offset.mar");
},
// Test collision detection where file A's indexes are a subset of file B's
test_collision_is_contained: function test_collision_is_contained() {
return extract_and_fail("manipulated_is_container.mar");
},
// Test collision detection where file B's indexes are a subset of file A's
test_collision_contained_by: function test_collision_contained_by() {
return extract_and_fail("manipulated_is_contained.mar");
},
// Test collision detection where file A ends in file B's indexes
test_collision_a_onto_b: function test_collision_a_onto_b() {
return extract_and_fail("manipulated_frontend_collision.mar");
},
// Test collision detection where file B ends in file A's indexes
test_collsion_b_onto_a: function test_collsion_b_onto_a() {
return extract_and_fail("manipulated_backend_collision.mar");
},
// Test collision detection where file C shares indexes with both file A & B
test_collision_multiple: function test_collision_multiple() {
return extract_and_fail("manipulated_multiple_collision.mar");
},
// Test collision detection where A is the last file in the list
test_collision_last: function test_collision_multiple_last() {
return extract_and_fail("manipulated_multiple_collision_last.mar");
},
// Test collision detection where A is the first file in the list
test_collision_first: function test_collision_multiple_first() {
return extract_and_fail("manipulated_multiple_collision_first.mar");
},
// Between each test make sure the out directory and its subfiles do
// not exist.
cleanup_per_test: function _cleanup_per_test() {