CVE-2019-3556 (C++, published 2021-10-26T20:15Z)
Description: value: HHVM supports the use of an "admin" server which accepts administrative requests over HTTP. One of those request handlers, dump-pcre-cache, can be used to output cached regular expressions from the current execution context into a file. The handler takes a parameter which specifies where on the filesystem to write this data. The parameter is not validated, allowing a malicious user to overwrite arbitrary files where the user running HHVM has write access. This issue affects HHVM versions prior to 4.56.2, all versions between 4.57.0 and 4.78.0, as well as 4.79.0, 4.80.0, 4.81.0, 4.82.0, and 4.83.0.
Severity: MEDIUM (CVSS v3 base score: 8.1)
Weakness: CWE-22 \u2014 Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
Repository: https://github.com/facebook/hhvm
Files changed:
hphp/runtime/base/preg.cpp
hphp/runtime/base/preg.h
hphp/runtime/base/runtime-option.cpp
hphp/runtime/base/runtime-option.h
hphp/runtime/base/string-util.cpp
hphp/runtime/ext/domdocument/ext_domdocument.h
hphp/runtime/ext/gd/libgd/gd.cpp
hphp/runtime/ext/ldap/ext_ldap.cpp
hphp/runtime/ext/xmlreader/ext_xmlreader.cpp
hphp/runtime/server/admin-request-handler.cpp
hphp/runtime/server/admin-request-handler.h
hphp/test/slow/ext_ldap/t78806688.php
hphp/test/slow/ext_ldap/t78806688.php.expectf
hphp/test/slow/ext_string/t76103217.php
hphp/test/slow/ext_string/t76103217.php.expect
hphp/test/slow/ext_xml/xmlreader_empty_expand.php
hphp/test/slow/ext_xml/xmlreader_empty_expand.php.expectf
hphp/util/light-process.cpp
hphp/util/light-process.h
hphp/zend/crypt-blowfish.cpp
hphp/zend/zend-string.cpp
Commit message: security fixes

https://hhvm.com/blog/2020/11/12/security-update.html


Vulnerable code:
  void dump(const std::string& filename);
void PCRECache::dump(const std::string& filename) {
  std::ofstream out(filename.c_str());
        out << it.first->data() << "\n";
          out << key.c_str() << "\n";
  out.close();
void pcre_dump_cache(const std::string& filename) {
  s_pcreCache.dump(filename);
 * Dump the contents of the PCRE cache to filename.
void pcre_dump_cache(const std::string& filename);
  if (salt && salt[0] == '\0') {
  req::ptr<XMLDocumentData> doc() const { return m_node->doc(); }
    + sy * (sizeof(int *) + sizeof(unsigned char *))
    + sx * sy * (sizeof(int) + sizeof(unsigned char));
  String result(3 * value.size(), ReserveString);
    docp = doc->docp();
    if (docp == nullptr) {
      auto filename = transport->getParam("file");
      if (filename == "") filename = "/tmp/static_strings";
      handleDumpStaticStringsRequest(cmd, filename);
      transport->sendString("OK\n");
      auto filename = transport->getParam("file");
      if (filename == "") filename = "/tmp/pcre_cache";
      pcre_dump_cache(filename);
      transport->sendString("OK\n");
bool AdminRequestHandler::handleDumpStaticStringsRequest(
  const std::string& /*cmd*/, const std::string& filename) {
  std::ofstream out(filename.c_str());
  SCOPE_EXIT { out.close(); };
    out << formatStaticString(item);
  bool handleDumpStaticStringsRequest(const std::string &cmd,
                                      const std::string &filename);
    // TODO(alexeyt) should we log something and/or fail to start?
    // TODO(alexeyt) should we log something and/or fail to start?

    initgroups(pw->pw_name, pw->pw_gid);
    setgid(pw->pw_gid);
    setuid(pw->pw_uid);
  if (count < min || BF_decode(data.binary.salt, &setting[7], 16)) {

  assert(key);
  assert(salt);
  if ((strlen(salt) > sizeof("$2X$00$")) &&
    if (php_crypt_blowfish_rn(key, salt, output, sizeof(output))) {

Fixed code:
#include <folly/FileUtil.h>
  void dump(folly::File& file);
void PCRECache::dump(folly::File& file) {
        folly::writeFull(file.fd(), it.first->data(), it.first->size());
        folly::writeFull(file.fd(), "\n", 1);
          folly::writeFull(file.fd(), key.data(), key.size());
          folly::writeFull(file.fd(), "\n", 1);
void pcre_dump_cache(folly::File& file) {
  s_pcreCache.dump(file);
#include <folly/File.h>
 * Dump the contents of the PCRE cache to the given file.
void pcre_dump_cache(folly::File& file);
#include "hphp/util/light-process.h"
std::string RuntimeOption::AdminDumpPath;
    Config::Bind(LightProcess::g_strictUser, ini, config,
                 "Server.LightProcessStrictUser", false);
    Config::Bind(AdminDumpPath, ini, config,
                 "AdminServer.DumpPath", "/tmp/hhvm_admin_dump");
  static std::string AdminDumpPath;

  assertx(salt);
  if (salt[0] == '\0') {
  req::ptr<XMLDocumentData> doc() const {
    return m_node ? m_node->doc() : nullptr;
  }
    + (sizeof(int *) + sizeof(unsigned char *)) * sy
    + (sizeof(int) + sizeof(unsigned char)) * sx * sy;
  String result(3UL * value.size(), ReserveString);
    if (doc == nullptr || doc->docp() == nullptr) {
    docp = doc->docp();
#include <folly/File.h>
#include <folly/FileUtil.h>
#include <folly/Optional.h>
namespace {

// When this struct is destroyed, it will close the file.
struct DumpFile {
  std::string path;
  folly::File file;
};

folly::Optional<DumpFile> dump_file(const char* name) {
  auto const path = folly::sformat("{}/{}", RO::AdminDumpPath, name);

  // mkdir -p the directory prefix of `path`
  if (FileUtil::mkdir(path) != 0) return folly::none;

  // If remove fails because of a permissions issue, then we won't be
  // able to open the file for exclusive write below.
  remove(path.c_str());

  // Create the file, failing if it already exists. Doing so ensures
  // that we have write access to the file and that no other user does.
  auto const fd = open(path.c_str(), O_CREAT|O_EXCL|O_RDWR, 0666);
  if (fd < 0) return folly::none;

  return DumpFile{path, folly::File(fd, /*owns=*/true)};
}

}

      if (auto file = dump_file("static_strings")) {
        handleDumpStaticStringsRequest(file->file);
        transport->sendString(folly::sformat("dumped to {}\n", file->path));
      } else {
        transport->sendString("Unable to mkdir or file already exists.\n");
      }
      if (auto file = dump_file("pcre_cache")) {
        pcre_dump_cache(file->file);
        transport->sendString(folly::sformat("dumped to {}\n", file->path));
      } else {
        transport->sendString("Unable to mkdir or file already exists.\n");
      }
bool AdminRequestHandler::handleDumpStaticStringsRequest(folly::File& file) {
    auto const line = formatStaticString(item);
    folly::writeFull(file.fd(), line.data(), line.size());
#include <folly/File.h>

  bool handleDumpStaticStringsRequest(folly::File& file);
<?hh

<<__EntryPoint>>
function main(): void {
  $multiplier = 1431655769;
  $s = str_repeat("a", $multiplier);
  ldap_escape($s);
  echo "FAIL!\n";
}
Fatal error: String length exceeded: 4294967307 > %d in %s/t78806688.php on line 7
<?hh

<<__EntryPoint>>
function main(): void {
  $key = "\x26\xbd\xbd\xbd\xff\x60\xbf\xff\xff\x60";
  $salt1 = "\x24\x32\x78\x24\x31\x30\x24\x24\x35\x24\xad\x20\x20\x26\xff\x60\xbf\xff\xff\x60\x24\x31\x78\xa8\xa8\xa0\x01\x01\x01\x01\x01\x01";
  $salt2 = "\x24\x32\x78\x24\x31\x30\x24\x24\x35";

  var_dump(base64_encode(crypt($key, $salt1)));
  var_dump(base64_encode(crypt($key, $salt2)));
}
string(4) "KjA="
string(12) "JDJ4JDEwJCQ1"
<?hh

<<__EntryPoint>>
function main(): void {
    $a = new XMLReader();
    $b = new DOMNode();
    $a->expand($b);
    $a->expand($a);
}
Warning: Invalid State Error in %s on line %d

Warning: Invalid State Error in %s on line %d
#include "hphp/util/struct-log.h"
bool LightProcess::g_strictUser = false;

  StructuredLogEntry* log = nullptr;
  int err = 0;
  SCOPE_EXIT {
    if (log) {
      log->setInt("errno", err);
      log->setStr("new_user", uname);
      StructuredLog::log("hhvm_lightprocess_error", *log);
      delete log;
    }
  };
    err = errno;
    log = new StructuredLogEntry();
    log->setStr("function", "getpwnam_r");
    if (LightProcess::g_strictUser) {
      throw std::runtime_error{"getpwnam_r(): " + folly::errnoStr(err)};
    }
    log = new StructuredLogEntry();
    log->setStr("function", "getpwnam_r");
    if (LightProcess::g_strictUser) {
      throw std::runtime_error{"getpwnam_r(): not found"};
    }
    if (initgroups(pw->pw_name, pw->pw_gid)) {
      err = errno;
      log = new StructuredLogEntry();
      log->setStr("function", "initgroups");
    }
    if (setgid(pw->pw_gid)) {
      if (!log) {
        err = errno;
        log = new StructuredLogEntry();
        log->setStr("function", "setgid");
        if (LightProcess::g_strictUser) {
          throw std::runtime_error{"setgid():" + folly::errnoStr(err)};
        }
      }
    }
    if (setuid(pw->pw_uid)) {
      if (!log) {
        err = errno;
        log = new StructuredLogEntry();
        log->setStr("function", "setuid");
        if (LightProcess::g_strictUser) {
          throw std::runtime_error{"setuid():" + folly::errnoStr(err)};
        }
      }
    }

 public:
  // whether to abort when user change fails.
  static bool g_strictUser;
  if (count < min ||
      BF_decode(data.binary.salt, &setting[7], 16) ||
      ((unsigned int)(setting[7 + 22 - 1] - 0x20) >= 0x60)
     ) {
  assertx(key);
  assertx(salt);
  auto const saltLen = strlen(salt);
  if ((saltLen > sizeof("$2X$00$")) &&

    static constexpr size_t maxSaltLength = 123;
    char paddedSalt[maxSaltLength + 1];
    paddedSalt[0] = paddedSalt[maxSaltLength] = '\0';

    memset(&paddedSalt[1], '$', maxSaltLength - 1);
    memcpy(paddedSalt, salt, std::min(maxSaltLength, saltLen));
    paddedSalt[saltLen] = '\0';

    if (php_crypt_blowfish_rn(key, paddedSalt, output, sizeof(output))) {