blob: 80f924f0154ed4c1a395edf3d8b023e33d4e6911 [file] [log] [blame]
// Copyright 2019 The Chromium OS 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 <dirent.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
#include <base/files/file_enumerator.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/posix/safe_strerror.h>
#include <base/strings/string_number_conversions.h>
#include <base/strings/string_util.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "debugd/src/crash_sender_tool.h"
#include "debugd/src/mock_process_with_id.h"
namespace debugd {
namespace {
using ::testing::_;
using ::testing::Invoke;
using ::testing::IsEmpty;
using ::testing::Pair;
using ::testing::Return;
using ::testing::SaveArg;
using ::testing::StartsWith;
using ::testing::TestParamInfo;
using ::testing::TestWithParam;
using ::testing::UnorderedElementsAre;
using ::testing::Values;
constexpr char kCrashDirectoryStart[] = "--crash_directory=/proc/self/fd/";
class CrashSenderToolWithMockCreateProcess : public CrashSenderTool {
public:
ProcessWithId* CreateProcess(bool sandboxed,
bool allow_root_mount_ns) override {
return &mock_process_;
}
MockProcessWithId& mock_process() { return mock_process_; }
// Expect all the args that CrashSenderTool always adds to ProcessWithId
// when it runs a process.
void ExpectStandardArgs() {
EXPECT_CALL(mock_process_, AddArg(_)).Times(0);
EXPECT_CALL(mock_process_, AddArg("/sbin/crash_sender")).Times(1);
EXPECT_CALL(mock_process_, AddArg("--max_spread_time=0")).Times(1);
EXPECT_CALL(mock_process_, AddArg("--ignore_rate_limits")).Times(1);
}
// Expect all the args that CrashSenderTool uses when invoking from
// UploadSingleCrash. Also, captures the argument to --crash_directory in
// |crash_directory_arg|.
void ExpectSingleCrashArgs(std::string* crash_directory_arg) {
ExpectStandardArgs();
EXPECT_CALL(mock_process_, AddArg("--ignore_hold_off_time")).Times(1);
EXPECT_CALL(mock_process_, AddArg(StartsWith(kCrashDirectoryStart)))
.WillOnce(SaveArg<0>(crash_directory_arg));
}
private:
MockProcessWithId mock_process_;
};
// Invoked during the call to mock_process_.Run(). Verifies that we were given
// a valid crash_directory argument, that the directory exists, and that the
// file descriptor points to the directory.
//
// |file_name_contents| will be filled in with file name + content pairs. Caller
// is responsible for using this to confirm that the correct set of files
// exists.
void VerifyStateInsideRun(
const std::string& crash_directory_arg,
const base::FilePath& expected_crash_directory,
std::vector<std::pair<std::string, std::string>>* file_name_contents) {
ASSERT_TRUE(base::DirectoryExists(expected_crash_directory));
std::vector<std::string> actual_file_names;
base::FileEnumerator file_list(
expected_crash_directory, false,
base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES);
for (base::FilePath name = file_list.Next(); !name.empty();
name = file_list.Next()) {
auto info = file_list.GetInfo();
EXPECT_FALSE(info.IsDirectory());
std::string contents;
EXPECT_TRUE(base::ReadFileToString(name, &contents));
file_name_contents->emplace_back(name.value(), std::move(contents));
}
// crash_directory_arg should contain --crash_directory=/proc/self/fd/##,
// where ## is a file descriptor that is open and reading /tmp/crash.1.
ASSERT_GT(crash_directory_arg.size(), strlen(kCrashDirectoryStart));
std::string fd_string =
crash_directory_arg.substr(strlen(kCrashDirectoryStart));
int fd = -1;
ASSERT_TRUE(base::StringToInt(fd_string, &fd));
struct stat stat_from_proc;
int fstat_result = fstat(fd, &stat_from_proc);
int saved_errno = errno;
ASSERT_NE(fstat_result, -1) << "Error stating passing-in file descriptor: "
<< base::safe_strerror(saved_errno);
struct stat stat_from_tmp;
int stat_result =
stat(expected_crash_directory.value().c_str(), &stat_from_tmp);
saved_errno = errno;
ASSERT_NE(stat_result, -1)
<< "Error stating /tmp directory: " << base::safe_strerror(saved_errno);
EXPECT_EQ(stat_from_tmp.st_dev, stat_from_proc.st_dev);
EXPECT_EQ(stat_from_tmp.st_ino, stat_from_proc.st_ino);
// In the real world, we would exec a new process during this call, so the
// file descriptor must not be FD_CLOEXEC.
int flags = fcntl(fd, F_GETFD);
saved_errno = errno;
EXPECT_NE(flags, -1) << "fnctl failed: " << base::safe_strerror(saved_errno);
EXPECT_EQ(flags & FD_CLOEXEC, 0);
}
// Creates a temporary file and returns a file descriptor to it. The file will
// contain |contents|.
//
// Ideally, we would use memfd_create here instead of creating a file on disk
// (to better match the actual expected usage of UploadSingleCrash), but some
// unit test environments don't support memfd_create.
base::ScopedFD CreateFileWithContents(const std::string& contents) {
base::FilePath temp_path;
EXPECT_TRUE(GetTempDir(&temp_path));
int fd =
open(temp_path.value().c_str(), O_RDWR | O_TMPFILE, S_IRUSR | S_IWUSR);
if (fd < 0) {
int saved_errno = errno;
ADD_FAILURE() << "Could not open temp file: "
<< base::safe_strerror(saved_errno);
return base::ScopedFD();
}
int result = write(fd, contents.data(), contents.size());
if (result == -1) {
int saved_errno = errno;
ADD_FAILURE() << "write to temp file: " << base::safe_strerror(saved_errno);
} else if (result < contents.size()) {
ADD_FAILURE() << "Partial write to temp file";
}
return base::ScopedFD(fd);
}
} // namespace
TEST(UploadCrashes, CallsCrashSenderWithoutCrashDirectoryOrIgnoreHoldOffTime) {
CrashSenderToolWithMockCreateProcess test_tool;
// No arguments except standard ones.
test_tool.ExpectStandardArgs();
EXPECT_CALL(test_tool.mock_process(), Run()).WillOnce(Return(0));
test_tool.UploadCrashes();
}
TEST(UploadSingleCrash, CreatesDirectory) {
const base::FilePath kExpectedCrashDirectory("/tmp/crash.1");
// Tests aren't as hermetic as we'd like; make sure that stale crash
// directories aren't left over from previous failures.
base::DeletePathRecursively(kExpectedCrashDirectory);
std::vector<std::tuple<std::string, base::ScopedFD>> files;
brillo::ErrorPtr error;
CrashSenderToolWithMockCreateProcess test_tool;
std::string crash_directory_arg;
test_tool.ExpectSingleCrashArgs(&crash_directory_arg);
auto run_state_verifier = [&crash_directory_arg, &kExpectedCrashDirectory]() {
std::vector<std::pair<std::string, std::string>> file_name_contents;
VerifyStateInsideRun(crash_directory_arg, kExpectedCrashDirectory,
&file_name_contents);
EXPECT_THAT(file_name_contents, IsEmpty());
return 0;
};
EXPECT_CALL(test_tool.mock_process(), Run())
.WillOnce(Invoke(run_state_verifier));
EXPECT_TRUE(test_tool.UploadSingleCrash(files, &error));
EXPECT_EQ(error.get(), nullptr);
// /tmp/crash.1 should now be deleted, and the file descriptor closed. We
// don't check for the file descriptor being closed since some other part of
// the system might have reused it.
EXPECT_FALSE(base::PathExists(kExpectedCrashDirectory));
}
TEST(UploadSingleCrash, CreatesFilesInDirectory) {
const base::FilePath kExpectedCrashDirectory("/tmp/crash.1");
base::DeletePathRecursively(kExpectedCrashDirectory);
std::vector<std::tuple<std::string, base::ScopedFD>> files;
constexpr char kFileAaaContents[] = "aaa";
files.emplace_back("aaa.meta", CreateFileWithContents(kFileAaaContents));
constexpr char kFileBbbContents[] = "123";
files.emplace_back("bbb.version", CreateFileWithContents(kFileBbbContents));
constexpr char kFileCccContents[] =
"The quick brown fox jumped over the lazy dog.";
files.emplace_back("ccc.log", CreateFileWithContents(kFileCccContents));
brillo::ErrorPtr error;
CrashSenderToolWithMockCreateProcess test_tool;
std::string crash_directory_arg;
test_tool.ExpectSingleCrashArgs(&crash_directory_arg);
std::vector<std::pair<std::string, std::string>> file_name_contents;
auto run_state_verifier = [&crash_directory_arg, &kExpectedCrashDirectory,
&file_name_contents]() {
VerifyStateInsideRun(crash_directory_arg, kExpectedCrashDirectory,
&file_name_contents);
return 0;
};
EXPECT_CALL(test_tool.mock_process(), Run())
.WillOnce(Invoke(run_state_verifier));
EXPECT_TRUE(test_tool.UploadSingleCrash(files, &error));
EXPECT_EQ(error.get(), nullptr);
EXPECT_FALSE(base::PathExists(kExpectedCrashDirectory));
EXPECT_THAT(
file_name_contents,
UnorderedElementsAre(Pair("/tmp/crash.1/aaa.meta", kFileAaaContents),
Pair("/tmp/crash.1/bbb.version", kFileBbbContents),
Pair("/tmp/crash.1/ccc.log", kFileCccContents)));
}
TEST(UploadSingleCrash, CreatesEmptyFile) {
const base::FilePath kExpectedCrashDirectory("/tmp/crash.1");
base::DeletePathRecursively(kExpectedCrashDirectory);
std::vector<std::tuple<std::string, base::ScopedFD>> files;
files.emplace_back("empty", base::ScopedFD(memfd_create("empty", 0)));
brillo::ErrorPtr error;
CrashSenderToolWithMockCreateProcess test_tool;
std::string crash_directory_arg;
test_tool.ExpectSingleCrashArgs(&crash_directory_arg);
std::vector<std::pair<std::string, std::string>> file_name_contents;
auto run_state_verifier = [&crash_directory_arg, &kExpectedCrashDirectory,
&file_name_contents]() {
VerifyStateInsideRun(crash_directory_arg, kExpectedCrashDirectory,
&file_name_contents);
return 0;
};
EXPECT_CALL(test_tool.mock_process(), Run())
.WillOnce(Invoke(run_state_verifier));
EXPECT_TRUE(test_tool.UploadSingleCrash(files, &error));
EXPECT_EQ(error.get(), nullptr);
EXPECT_FALSE(base::PathExists(kExpectedCrashDirectory));
EXPECT_THAT(file_name_contents,
UnorderedElementsAre(Pair("/tmp/crash.1/empty", "")));
}
TEST(UploadSingleCrash, CreatesLargeFilesCorrectly) {
const base::FilePath kExpectedCrashDirectory("/tmp/crash.1");
base::DeletePathRecursively(kExpectedCrashDirectory);
std::vector<std::tuple<std::string, base::ScopedFD>> files;
std::string long_string;
constexpr int kSize = 1 << 18;
long_string.reserve(kSize);
int i = 0;
while (long_string.size() < kSize) {
long_string += base::NumberToString(i);
i++;
}
files.emplace_back("long.log", CreateFileWithContents(long_string));
constexpr char kFileAaaContents[] = "aaa";
files.emplace_back("aaa.meta", CreateFileWithContents(kFileAaaContents));
brillo::ErrorPtr error;
CrashSenderToolWithMockCreateProcess test_tool;
std::string crash_directory_arg;
test_tool.ExpectSingleCrashArgs(&crash_directory_arg);
std::vector<std::pair<std::string, std::string>> file_name_contents;
auto run_state_verifier = [&crash_directory_arg, &kExpectedCrashDirectory,
&file_name_contents]() {
VerifyStateInsideRun(crash_directory_arg, kExpectedCrashDirectory,
&file_name_contents);
return 0;
};
EXPECT_CALL(test_tool.mock_process(), Run())
.WillOnce(Invoke(run_state_verifier));
EXPECT_TRUE(test_tool.UploadSingleCrash(files, &error));
EXPECT_EQ(error.get(), nullptr);
EXPECT_FALSE(base::PathExists(kExpectedCrashDirectory));
EXPECT_THAT(
file_name_contents,
UnorderedElementsAre(Pair("/tmp/crash.1/aaa.meta", kFileAaaContents),
Pair("/tmp/crash.1/long.log", long_string)));
}
// Test that the filename given by the parameter will be rejected with a
// org.chromium.debugd.error.BadFileName error.
class BadFilenameTest : public TestWithParam<std::string> {
public:
BadFilenameTest() : file_name_(GetParam()) {}
protected:
// The bad file name. Taken from GetParam.
const std::string file_name_;
};
TEST_P(BadFilenameTest, ReturnsBadFileNameError) {
std::vector<std::tuple<std::string, base::ScopedFD>> files;
files.emplace_back(file_name_, CreateFileWithContents("something"));
brillo::ErrorPtr error;
CrashSenderToolWithMockCreateProcess test_tool;
// We should NOT run if there's a bad file name
EXPECT_CALL(test_tool.mock_process(), Run()).Times(0);
EXPECT_FALSE(test_tool.UploadSingleCrash(files, &error));
ASSERT_TRUE(error);
EXPECT_EQ(error->GetCode(), CrashSenderTool::kErrorBadFileName);
}
std::string FileNameToTestName(const TestParamInfo<std::string>& param) {
// Result must be alphanumeric only. No _'s or other symbols, and no spaces.
std::string result;
for (char c : param.param) {
switch (c) {
case '/':
result += "slash";
break;
case '.':
result += "dot";
break;
default:
if (base::IsAsciiAlpha(c) || base::IsAsciiDigit(c)) {
result += c;
} else {
result += "something";
}
break;
}
}
return result;
}
INSTANTIATE_TEST_SUITE_P(
BadFilenameTests,
BadFilenameTest,
Values("/tmp/absolute", ".", "..", "../backup", "non/basename", "/", "//"),
FileNameToTestName);
} // namespace debugd