CVE-2020-1898 (C, published 2021-03-11T01:15Z)
Description: value: The fb_unserialize function did not impose a depth limit for nested deserialization. That meant a maliciously constructed string could cause deserialization to recurse, leading to stack exhaustion. This issue affected HHVM prior to v4.32.3, between versions 4.33.0 and 4.56.0, 4.57.0, 4.58.0, 4.58.1, 4.59.0, 4.60.0, 4.61.0, 4.62.0.
Severity: MEDIUM (CVSS v3 base score: 7.5)
Weakness: CWE-674 \u2014 Uncontrolled Recursion
The product does not properly control the amount of recursion that takes place,  consuming excessive resources, such as allocated memory or the program stack.
Repository: https://github.com/facebook/hhvm
Files changed:
hphp/hack/hhi/builtins_fb.hhi
hphp/runtime/ext/fb/FBSerialize/FBSerialize-inl.h
hphp/runtime/ext/fb/FBSerialize/FBSerialize.h
hphp/runtime/ext/fb/VariantController.h
hphp/runtime/ext/fb/ext_fb.cpp
hphp/test/slow/ext_fb/unserialize-bad-set.php
hphp/test/slow/ext_fb/unserialize-bad-set.php.expect
hphp/test/slow/ext_fb/unserialize_max_depth.php
hphp/test/slow/ext_fb/unserialize_max_depth.php.expect
Commit message: CVE-2020-1898

Don't allow for unlimited nesting in FBUnserialize or fb_compact_unserialize


Vulnerable code:
  return unserializer.unserializeThing();
inline typename V::MapType FBUnserializer<V>::unserializeMap() {
          auto value = unserializeThing();
          auto value = unserializeThing();
inline typename V::VectorType FBUnserializer<V>::unserializeVector() {
    V::vectorAppend(ret, unserializeThing());
inline typename V::VectorType FBUnserializer<V>::unserializeList() {
    V::vectorAppend(ret, unserializeThing());
inline typename V::SetType FBUnserializer<V>::unserializeSet() {
    V::setAppend(ret, unserializeThing());
inline typename V::VariantType FBUnserializer<V>::unserializeThing() {
      return V::fromMap(unserializeMap());
      return V::fromVector(unserializeVector());
      return V::fromVector(unserializeList());
      return V::fromSet(unserializeSet());
  typename V::MapType unserializeMap();
  typename V::VectorType unserializeVector();
  typename V::VectorType unserializeList();
  typename V::SetType unserializeSet();
  typename V::VariantType unserializeThing();
    auto value_type = type(v);
    if (value_type != HPHP::serialize::Type::INT64 &&
        value_type != HPHP::serialize::Type::STRING) {
          "Unsupported keyset element of type " +
          folly::to<std::string>(value_type));
  Variant& out, const char* buf, int n, int& p) {
        int err = fb_compact_unserialize_from_buffer(value, buf, n, p);
          int err = fb_compact_unserialize_from_buffer(value, buf, n, p);
        int err = fb_compact_unserialize_from_buffer(key, buf, n, p);
        err = fb_compact_unserialize_from_buffer(value, buf, n, p);
  int err = fb_compact_unserialize_from_buffer(ret, str, len, p);

Fixed code:
const FB_UNSERIALIZE_MAX_DEPTH_EXCEEDED = 0;
  return unserializer.unserializeThing(0);
inline typename V::MapType FBUnserializer<V>::unserializeMap(size_t depth) {
          auto value = unserializeThing(depth + 1);
          auto value = unserializeThing(depth + 1);
inline typename V::VectorType
FBUnserializer<V>::unserializeVector(size_t depth) {
    V::vectorAppend(ret, unserializeThing(depth + 1));
inline typename V::VectorType FBUnserializer<V>::unserializeList(size_t depth) {
    V::vectorAppend(ret, unserializeThing(depth + 1));
inline typename V::SetType FBUnserializer<V>::unserializeSet(size_t depth) {
    V::setAppend(ret, unserializeThing(depth + 1));
inline typename V::VariantType
FBUnserializer<V>::unserializeThing(size_t depth) {
  if (UNLIKELY(depth > 1024)) {
    throw UnserializeError("depth > 1024");
  }

      return V::fromMap(unserializeMap(depth));
      return V::fromVector(unserializeVector(depth));
      return V::fromVector(unserializeList(depth));
      return V::fromSet(unserializeSet(depth));
  typename V::MapType unserializeMap(size_t depth);
  typename V::VectorType unserializeVector(size_t depth);
  typename V::VectorType unserializeList(size_t depth);
  typename V::SetType unserializeSet(size_t depth);
  typename V::VariantType unserializeThing(size_t depth);
    if (!v.isInteger() && !v.isString()) {
        "Keysets can only contain integers or strings"
      );
#define FB_UNSERIALIZE_MAX_DEPTH_EXCEEDED        0x0005
    Variant& out, const char* buf, int n, int& p, size_t depth) {
  if (UNLIKELY(depth > 1024)) {
    return FB_UNSERIALIZE_MAX_DEPTH_EXCEEDED;
  }
        int err =
          fb_compact_unserialize_from_buffer(value, buf, n, p, depth + 1);
          int err =
            fb_compact_unserialize_from_buffer(value, buf, n, p, depth + 1);
        int err = fb_compact_unserialize_from_buffer(key, buf, n, p, depth + 1);
        err = fb_compact_unserialize_from_buffer(value, buf, n, p, depth + 1);
  int err = fb_compact_unserialize_from_buffer(ret, str, len, p, 0);
    HHVM_RC_INT_SAME(FB_UNSERIALIZE_MAX_DEPTH_EXCEEDED);
<?hh
// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.

<<__EntryPoint>>
function main() {
  $ret = null;
  var_dump(fb_unserialize("\x14\x02\x01\x14\x02\x01\x02\x01\x01\x01", inout $ret));
  var_dump($ret);
}
bool(false)
bool(false)
<?hh
// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.

function make_fb_serialize_struct($size) {
  return str_repeat("\x0a\x02\x01", $size) . "\x02\x01" . str_repeat("\x01", $size);
}

function make_fb_serialize_vector($size) {
  return str_repeat("\x12", $size) . str_repeat("\x01", $size);
}

function make_fb_serialize_list($size) {
  return str_repeat("\x13\x02\x01", $size) . "\x02\x01" . str_repeat("\x01", $size);
}

function make_fb_cs_list_map($size) {
  return str_repeat("\xfa\xfd", $size) . str_repeat("\xfc", $size);
}

function make_fb_cs_map($size) {
  return str_repeat("\xfb\x01", $size) . "\x01" . str_repeat("\xfc", $size);
}

function make_fb_cs_vector($size) {
  return str_repeat("\xfe", $size) . str_repeat("\xfc", $size);
}

function test($serialized) {
  $ret = null;
  var_dump(
    fb_unserialize(
      $serialized,
      inout $ret,
      FB_SERIALIZE_HACK_ARRAYS
    )
  );
  var_dump($ret);
}

function tests($size) {
  test(make_fb_serialize_struct($size));
  test(make_fb_serialize_vector($size));
  test(make_fb_serialize_list($size));
  test(make_fb_cs_list_map($size));
  test(make_fb_cs_map($size));
  test(make_fb_cs_vector($size));
}

<<__EntryPoint>>
function main() {
  tests(10);
  tests(1026);
}
dict(1) {
  [1]=>
  dict(1) {
    [1]=>
    dict(1) {
      [1]=>
      dict(1) {
        [1]=>
        dict(1) {
          [1]=>
          dict(1) {
            [1]=>
            dict(1) {
              [1]=>
              dict(1) {
                [1]=>
                dict(1) {
                  [1]=>
                  dict(1) {
                    [1]=>
                    int(1)
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
bool(true)
vec(1) {
  vec(1) {
    vec(1) {
      vec(1) {
        vec(1) {
          vec(1) {
            vec(1) {
              vec(1) {
                vec(1) {
                  vec(0) {
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
bool(true)
vec(1) {
  vec(1) {
    vec(1) {
      vec(1) {
        vec(1) {
          vec(1) {
            vec(1) {
              vec(1) {
                vec(1) {
                  vec(1) {
                    int(1)
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
bool(true)
array(1) {
  [1]=>
  array(1) {
    [1]=>
    array(1) {
      [1]=>
      array(1) {
        [1]=>
        array(1) {
          [1]=>
          array(1) {
            [1]=>
            array(1) {
              [1]=>
              array(1) {
                [1]=>
                array(1) {
                  [1]=>
                  array(0) {
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
bool(true)
array(1) {
  [1]=>
  array(1) {
    [1]=>
    array(1) {
      [1]=>
      array(1) {
        [1]=>
        array(1) {
          [1]=>
          array(1) {
            [1]=>
            array(1) {
              [1]=>
              array(1) {
                [1]=>
                array(1) {
                  [1]=>
                  array(1) {
                    [1]=>
                    int(1)
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
bool(true)
array(1) {
  [0]=>
  array(1) {
    [0]=>
    array(1) {
      [0]=>
      array(1) {
        [0]=>
        array(1) {
          [0]=>
          array(1) {
            [0]=>
            array(1) {
              [0]=>
              array(1) {
                [0]=>
                array(1) {
                  [0]=>
                  array(0) {
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
bool(true)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)