blob: 31f4e641b69e28435d12061a571672dcf22eafa8 [file] [log] [blame]
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#define LOG_TAG "FileSystemUtils"
#include "com_android_internal_content_FileSystemUtils.h"
#include <android-base/file.h>
#include <android-base/hex.h>
#include <android-base/unique_fd.h>
#include <bionic/macros.h>
#include <elf.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <linux/fs.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <utils/Log.h>
#include <array>
#include <fstream>
#include <vector>
using android::base::borrowed_fd;
using android::base::HexString;
using android::base::ReadFullyAtOffset;
namespace android {
bool punchWithBlockAlignment(borrowed_fd fd, uint64_t start, uint64_t length, uint64_t blockSize) {
uint64_t end;
if (__builtin_add_overflow(start, length, &end)) {
ALOGE("Overflow occurred when calculating end");
return false;
}
start = align_up(start, blockSize);
end = align_down(end, blockSize);
uint64_t alignedLength;
if (__builtin_sub_overflow(end, start, &alignedLength)) {
ALOGE("Overflow occurred when calculating length");
return false;
}
if (alignedLength < blockSize) {
ALOGW("Skipping punching hole as aligned length is less than block size");
return false;
}
ALOGD("Punching hole in file - start: %" PRIu64 " len:%" PRIu64 "", start, alignedLength);
int result =
fallocate(fd.get(), FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, start, alignedLength);
if (result < 0) {
ALOGE("fallocate failed to punch hole, error:%d", errno);
return false;
}
return true;
}
bool punchHoles(const char *filePath, const uint64_t offset,
const std::vector<Elf64_Phdr> &programHeaders) {
struct stat64 beforePunch;
if (int result = lstat64(filePath, &beforePunch); result != 0) {
ALOGE("lstat64 failed for filePath %s, error:%d", filePath, errno);
return false;
}
uint64_t blockSize = beforePunch.st_blksize;
IF_ALOGD() {
ALOGD("Total number of LOAD segments %zu", programHeaders.size());
ALOGD("Size before punching holes st_blocks: %" PRIu64
", st_blksize: %ld, st_size: %" PRIu64 "",
beforePunch.st_blocks, beforePunch.st_blksize,
static_cast<uint64_t>(beforePunch.st_size));
}
android::base::unique_fd fd(open(filePath, O_RDWR | O_CLOEXEC));
if (!fd.ok()) {
ALOGE("Can't open file to punch %s", filePath);
return false;
}
// read in chunks of 64KB
constexpr uint64_t kChunkSize = 64 * 1024;
// malloc is used to gracefully handle oom which might occur during the allocation of buffer.
// allocating using new or vector here results in oom/exception on failure where as malloc will
// return nullptr.
std::unique_ptr<uint8_t, decltype(&free)> buffer(static_cast<uint8_t *>(malloc(kChunkSize)),
&free);
if (buffer == nullptr) {
ALOGE("Failed to allocate read buffer");
return false;
}
for (size_t index = 0; programHeaders.size() >= 2 && index < programHeaders.size() - 1;
index++) {
// find LOAD segments from program headers, calculate padding and punch holes
uint64_t punchOffset;
if (__builtin_add_overflow(programHeaders[index].p_offset, programHeaders[index].p_filesz,
&punchOffset)) {
ALOGE("Overflow occurred when adding offset and filesize");
return false;
}
uint64_t punchLen;
if (__builtin_sub_overflow(programHeaders[index + 1].p_offset, punchOffset, &punchLen)) {
ALOGE("Overflow occurred when calculating length");
return false;
}
if (punchLen < blockSize) {
continue;
}
// if we have a uncompressed file which is being opened from APK, use the offset to
// punch native lib inside Apk.
uint64_t punchStartOffset;
if (__builtin_add_overflow(offset, punchOffset, &punchStartOffset)) {
ALOGE("Overflow occurred when calculating length");
return false;
}
uint64_t position = punchStartOffset;
uint64_t endPosition;
if (__builtin_add_overflow(position, punchLen, &endPosition)) {
ALOGE("Overflow occurred when calculating length");
return false;
}
// Read content in kChunkSize and verify it is zero
while (position <= endPosition) {
uint64_t uncheckedChunkEnd;
if (__builtin_add_overflow(position, kChunkSize, &uncheckedChunkEnd)) {
ALOGE("Overflow occurred when calculating uncheckedChunkEnd");
return false;
}
uint64_t readLength;
if (__builtin_sub_overflow(std::min(uncheckedChunkEnd, endPosition), position,
&readLength)) {
ALOGE("Overflow occurred when calculating readLength");
return false;
}
if (!ReadFullyAtOffset(fd, buffer.get(), readLength, position)) {
ALOGE("Failed to read content to punch holes");
return false;
}
IF_ALOGD() {
ALOGD("Punching holes for length:%" PRIu64 " content which should be zero: %s",
readLength, HexString(buffer.get(), readLength).c_str());
}
bool isZero = std::all_of(buffer.get(), buffer.get() + readLength,
[](uint8_t i) constexpr { return i == 0; });
if (!isZero) {
ALOGE("Found non zero content while trying to punch hole. Skipping operation");
return false;
}
position = uncheckedChunkEnd;
}
if (!punchWithBlockAlignment(fd, punchStartOffset, punchLen, blockSize)) {
return false;
}
}
IF_ALOGD() {
struct stat64 afterPunch;
if (int result = lstat64(filePath, &afterPunch); result != 0) {
ALOGD("lstat64 failed for filePath %s, error:%d", filePath, errno);
return false;
}
ALOGD("Size after punching holes st_blocks: %" PRIu64 ", st_blksize: %ld, st_size: %" PRIu64
"",
afterPunch.st_blocks, afterPunch.st_blksize,
static_cast<uint64_t>(afterPunch.st_size));
}
return true;
}
bool punchHolesInElf64(const char *filePath, const uint64_t offset) {
// Open Elf file
Elf64_Ehdr ehdr;
std::ifstream inputStream(filePath, std::ifstream::in);
// If this is a zip file, set the offset so that we can read elf file directly
inputStream.seekg(offset);
// read executable headers
inputStream.read((char *)&ehdr, sizeof(ehdr));
if (!inputStream.good()) {
return false;
}
// only consider elf64 for punching holes
if (ehdr.e_ident[EI_CLASS] != ELFCLASS64) {
ALOGW("Provided file is not ELF64");
return false;
}
// read the program headers from elf file
uint64_t programHeaderOffset = ehdr.e_phoff;
uint16_t programHeaderNum = ehdr.e_phnum;
IF_ALOGD() {
ALOGD("Punching holes in file: %s programHeaderOffset: %" PRIu64 " programHeaderNum: %hu",
filePath, programHeaderOffset, programHeaderNum);
}
// if this is a zip file, also consider elf offset inside a file
uint64_t phOffset;
if (__builtin_add_overflow(offset, programHeaderOffset, &phOffset)) {
ALOGE("Overflow occurred when calculating phOffset");
return false;
}
inputStream.seekg(phOffset);
std::vector<Elf64_Phdr> programHeaders;
for (int headerIndex = 0; headerIndex < programHeaderNum; headerIndex++) {
Elf64_Phdr header;
inputStream.read((char *)&header, sizeof(header));
if (!inputStream.good()) {
return false;
}
if (header.p_type != PT_LOAD) {
continue;
}
programHeaders.push_back(header);
}
return punchHoles(filePath, offset, programHeaders);
}
bool punchHolesInZip(const char *filePath, uint64_t offset, uint16_t extraFieldLen) {
android::base::unique_fd fd(open(filePath, O_RDWR | O_CLOEXEC));
if (!fd.ok()) {
ALOGE("Can't open file to punch %s", filePath);
return false;
}
struct stat64 beforePunch;
if (int result = lstat64(filePath, &beforePunch); result != 0) {
ALOGE("lstat64 failed for filePath %s, error:%d", filePath, errno);
return false;
}
uint64_t blockSize = beforePunch.st_blksize;
IF_ALOGD() {
ALOGD("Extra field length: %hu, Size before punching holes st_blocks: %" PRIu64
", st_blksize: %ld, st_size: %" PRIu64 "",
extraFieldLen, beforePunch.st_blocks, beforePunch.st_blksize,
static_cast<uint64_t>(beforePunch.st_size));
}
if (extraFieldLen < blockSize) {
ALOGD("Skipping punching apk as extra field length is less than block size");
return false;
}
// content is preceded by extra field. Zip offset is offset of exact content.
// move back by extraFieldLen so that scan can be started at start of extra field.
uint64_t extraFieldStart;
if (__builtin_sub_overflow(offset, extraFieldLen, &extraFieldStart)) {
ALOGE("Overflow occurred when calculating start of extra field");
return false;
}
constexpr uint64_t kMaxSize = 64 * 1024;
// Use malloc to gracefully handle any oom conditions
std::unique_ptr<uint8_t, decltype(&free)> buffer(static_cast<uint8_t *>(malloc(kMaxSize)),
&free);
if (buffer == nullptr) {
ALOGE("Failed to allocate read buffer");
return false;
}
// Read the entire extra fields at once and punch file according to zero stretches.
if (!ReadFullyAtOffset(fd, buffer.get(), extraFieldLen, extraFieldStart)) {
ALOGE("Failed to read extra field content");
return false;
}
IF_ALOGD() {
ALOGD("Extra field length: %hu content near offset: %s", extraFieldLen,
HexString(buffer.get(), extraFieldLen).c_str());
}
uint64_t currentSize = 0;
while (currentSize < extraFieldLen) {
uint64_t end = currentSize;
// find zero ranges
while (end < extraFieldLen && *(buffer.get() + end) == 0) {
++end;
}
uint64_t punchLen;
if (__builtin_sub_overflow(end, currentSize, &punchLen)) {
ALOGW("Overflow occurred when calculating punching length");
return false;
}
// Don't punch for every stretch of zero which is found
if (punchLen > blockSize) {
uint64_t punchOffset;
if (__builtin_add_overflow(extraFieldStart, currentSize, &punchOffset)) {
ALOGW("Overflow occurred when calculating punch start offset");
return false;
}
if (!punchWithBlockAlignment(fd, punchOffset, punchLen, blockSize)) {
return false;
}
}
currentSize = end;
++currentSize;
}
IF_ALOGD() {
struct stat64 afterPunch;
if (int result = lstat64(filePath, &afterPunch); result != 0) {
ALOGD("lstat64 failed for filePath %s, error:%d", filePath, errno);
return false;
}
ALOGD("punchHolesInApk:: Size after punching holes st_blocks: %" PRIu64
", st_blksize: %ld, st_size: %" PRIu64 "",
afterPunch.st_blocks, afterPunch.st_blksize,
static_cast<uint64_t>(afterPunch.st_size));
}
return true;
}
}; // namespace android