Commit message: CVE-2019-3557: Fix OOB read in readRecord on BZ2Files/OutputFiles

Summary:
These File subclasses return -1 on read errors which is not what is
expected for readImpl--this made File::readRecord behave unusually if the read
fails, causing it to read (size_t)(-1) bytes from its stream buffer; which,
unsurprisingly produces a out-of-bounds heap read.

Reviewed By: leikahing, jjgriego

Differential Revision: D13659395

fbshipit-source-id: 359ed6e3ff9f9cf49b752b666f51c4e0b3ce4b8a
CVE: CVE-2019-3557
CWE: CWE-125
Vulnerability description: The implementations of streams for bz2 and php://output improperly implemented their readImpl functions, returning -1 consistently. This behavior caused some stream functions, such as stream_get_line, to trigger an out-of-bounds read when operating on such malformed streams. The implementations were updated to return valid values consistently. This affects all supported versions of HHVM (3.30 and 3.27.4 and below).

Project: hhvm
File: bz2-file.cpp

int64_t BZ2File::errnu() {
  assertx(m_bzFile);
  int errnum = 0;
  BZ2_bzerror(m_bzFile, &errnum);
  return errnum;
}

Project: hhvm
File: output-file.cpp

bool OutputFile::close() {
  invokeFiltersOnClose();
  return closeImpl();
}

Project: hhvm
File: bz2-file.cpp

BZ2File::~BZ2File() {
  closeImpl();
}

Project: hhvm
File: output-file.cpp

bool OutputFile::seek(int64_t /*offset*/, int /*whence*/ /* = SEEK_SET */) {
  raise_warning("cannot seek a php://output stream");
  return false;
}

Project: hhvm
File: output-file.cpp

int64_t OutputFile::writeImpl(const char *buffer, int64_t length) {
  assertx(length > 0);
  if (isClosed()) return 0;
  g_context->write(buffer, length);
  return length;
}

Project: hhvm
File: output-file.cpp

bool OutputFile::closeImpl() {
  *s_pcloseRet = 0;
  if (!isClosed()) {
    setIsClosed(true);
    return true;
  }
  return false;
}

Project: hhvm
File: output-file.cpp

bool OutputFile::flush() {
  if (!isClosed()) {
    g_context->flush();
    return true;
  }
  return false;
}

Project: hhvm
File: bz2-file.cpp

Array BZ2File::error() {
  assertx(m_bzFile);
  int errnum;
  const char * errstr;
  errstr = BZ2_bzerror(m_bzFile, &errnum);
  return make_map_array(s_errno, errnum, s_errstr, String(errstr));
}

Project: hhvm
File: output-file.cpp

bool OutputFile::rewind() {
  raise_warning("cannot rewind a php://output stream");
  return false;
}

Project: hhvm
File: bz2-file.cpp

bool BZ2File::close() {
  invokeFiltersOnClose();
  return closeImpl();
}

Project: hhvm
File: output-file.cpp

bool OutputFile::truncate(int64_t /*size*/) {
  raise_warning("cannot truncate a php://output stream");
  return false;
}

Project: hhvm
File: bz2-file.cpp

int64_t BZ2File::writeImpl(const char * buf, int64_t length) {
  assertx(m_bzFile);
  return BZ2_bzwrite(m_bzFile, (char *)buf, length);
}

Project: hhvm
File: output-file.cpp

OutputFile::OutputFile(const String& filename): File(true, s_php, s_output) {
  if (filename != s_php_output) {
    raise_fatal_error("not a php://output file ");
  }
  setIsLocal(true);
}

Project: hhvm
File: bz2-file.cpp

bool BZ2File::open(const String& filename, const String& mode) {
  assertx(m_bzFile == nullptr);

  return m_innerFile->open(filename, mode) &&
    (m_bzFile = BZ2_bzdopen(dup(m_innerFile->fd()), mode.data()));
}

Project: hhvm
File: bz2-file.cpp

String BZ2File::errstr() {
  assertx(m_bzFile);
  int errnum;
  return BZ2_bzerror(m_bzFile, &errnum);
}

Project: hhvm
File: bz2-file.cpp

bool BZ2File::closeImpl() {
  if (!isClosed()) {
    if (m_bzFile) {
      BZ2_bzclose(m_bzFile);
      m_bzFile = nullptr;
    }
    setIsClosed(true);
    if (m_innerFile) {
      m_innerFile->close();
    }
  }
  File::closeImpl();
  return true;
}

Project: hhvm
File: output-file.cpp

int64_t OutputFile::tell() {
  raise_warning("cannot tell a php://output stream");
  return -1;
}

Project: hhvm
File: bz2-file.cpp

int64_t BZ2File::readImpl(char * buf, int64_t length) {
  if (length == 0) {
    return 0;
  }
  assertx(m_bzFile);
  int len = BZ2_bzread(m_bzFile, buf, length);
  /* Sometimes libbz2 will return fewer bytes than requested, and set bzerror
   * to BZ_STREAM_END, but it's not actually EOF, and you can keep reading from
   * the file - so, only set EOF after a failed read. This matches PHP5.
   */
  if (len <= 0) {
    setEof(true);
    if (len < 0) {
      return 0;
    }
  }
  return len;
}

Project: hhvm
File: output-file.cpp

int64_t OutputFile::readImpl(char* /*buffer*/, int64_t /*length*/) {
  raise_warning("cannot read from a php://output stream");
  return 0;
}

Project: hhvm
File: bz2-file.cpp

BZ2File::BZ2File(): m_bzFile(nullptr), m_innerFile(req::make<PlainFile>()) {
  m_innerFile->unregister();
  setIsLocal(m_innerFile->isLocal());
}

Project: hhvm
File: bz2-file.cpp

BZ2File::BZ2File(req::ptr<PlainFile>&& innerFile)
: m_bzFile(nullptr), m_innerFile(std::move(innerFile)) {
  setIsLocal(m_innerFile->isLocal());
}

Project: hhvm
File: output-file.cpp

bool OutputFile::open(const String& /*filename*/, const String& /*mode*/) {
  raise_fatal_error("cannot open a php://output file ");
}

Project: hhvm
File: output-file.cpp

void OutputFile::sweep() {
  closeImpl();
  File::sweep();
}

Project: hhvm
File: output-file.cpp

int OutputFile::getc() {
  raise_warning("cannot read from a php://output stream");
  return -1;
}

Project: hhvm
File: output-file.cpp

bool OutputFile::eof() {
  return false;
}

Project: hhvm
File: bz2-file.cpp

bool BZ2File::eof() {
  assertx(m_bzFile);
  return getEof();
}

Project: hhvm
File: bz2-file.cpp

bool BZ2File::flush() {
  assertx(m_bzFile);
  return BZ2_bzflush(m_bzFile);
}

Project: hhvm
File: bz2-file.cpp

void BZ2File::sweep() {
  closeImpl();
  File::sweep();
}

Project: hhvm
File: output-file.cpp

OutputFile::~OutputFile() {
  OutputFile::closeImpl();
}