//==- CheckPlacementNew.cpp - Check for placement new operation --*- C++ -*-==//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
//  This file defines a check for misuse of the default placement new operator.
//
//===----------------------------------------------------------------------===//

#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"
#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/DynamicSize.h"
#include "llvm/Support/FormatVariadic.h"

using namespace clang;
using namespace ento;

namespace {
class PlacementNewChecker : public Checker<check::PreStmt<CXXNewExpr>> {
public:
  void checkPreStmt(const CXXNewExpr *NE, CheckerContext &C) const;

private:
  bool checkPlaceCapacityIsSufficient(const CXXNewExpr *NE,
                                      CheckerContext &C) const;

  bool checkPlaceIsAlignedProperly(const CXXNewExpr *NE,
                                   CheckerContext &C) const;

  // Returns the size of the target in a placement new expression.
  // E.g. in "new (&s) long" it returns the size of `long`.
  SVal getExtentSizeOfNewTarget(const CXXNewExpr *NE, CheckerContext &C,
                                bool &IsArray) const;
  // Returns the size of the place in a placement new expression.
  // E.g. in "new (&s) long" it returns the size of `s`.
  SVal getExtentSizeOfPlace(const CXXNewExpr *NE, CheckerContext &C) const;

  void emitBadAlignReport(const Expr *P, CheckerContext &C,
                          unsigned AllocatedTAlign,
                          unsigned StorageTAlign) const;
  unsigned getStorageAlign(CheckerContext &C, const ValueDecl *VD) const;

  void checkElementRegionAlign(const ElementRegion *R, CheckerContext &C,
                               const Expr *P, unsigned AllocatedTAlign) const;

  void checkFieldRegionAlign(const FieldRegion *R, CheckerContext &C,
                             const Expr *P, unsigned AllocatedTAlign) const;

  bool isVarRegionAlignedProperly(const VarRegion *R, CheckerContext &C,
                                  const Expr *P,
                                  unsigned AllocatedTAlign) const;

  BugType SBT{this, "Insufficient storage for placement new",
              categories::MemoryError};
  BugType ABT{this, "Bad align storage for placement new",
              categories::MemoryError};
};
} // namespace

SVal PlacementNewChecker::getExtentSizeOfPlace(const CXXNewExpr *NE,
                                               CheckerContext &C) const {
  const Expr *Place = NE->getPlacementArg(0);
  return getDynamicSizeWithOffset(C.getState(), C.getSVal(Place));
}

SVal PlacementNewChecker::getExtentSizeOfNewTarget(const CXXNewExpr *NE,
                                                   CheckerContext &C,
                                                   bool &IsArray) const {
  ProgramStateRef State = C.getState();
  SValBuilder &SvalBuilder = C.getSValBuilder();
  QualType ElementType = NE->getAllocatedType();
  ASTContext &AstContext = C.getASTContext();
  CharUnits TypeSize = AstContext.getTypeSizeInChars(ElementType);
  IsArray = false;
  if (NE->isArray()) {
    IsArray = true;
    const Expr *SizeExpr = *NE->getArraySize();
    SVal ElementCount = C.getSVal(SizeExpr);
    if (auto ElementCountNL = ElementCount.getAs<NonLoc>()) {
      // size in Bytes = ElementCountNL * TypeSize
      return SvalBuilder.evalBinOp(
          State, BO_Mul, *ElementCountNL,
          SvalBuilder.makeArrayIndex(TypeSize.getQuantity()),
          SvalBuilder.getArrayIndexType());
    }
  } else {
    // Create a concrete int whose size in bits and signedness is equal to
    // ArrayIndexType.
    llvm::APInt I(AstContext.getTypeSizeInChars(SvalBuilder.getArrayIndexType())
                          .getQuantity() *
                      C.getASTContext().getCharWidth(),
                  TypeSize.getQuantity());
    return SvalBuilder.makeArrayIndex(I.getZExtValue());
  }
  return UnknownVal();
}

bool PlacementNewChecker::checkPlaceCapacityIsSufficient(
    const CXXNewExpr *NE, CheckerContext &C) const {
  bool IsArrayTypeAllocated;
  SVal SizeOfTarget = getExtentSizeOfNewTarget(NE, C, IsArrayTypeAllocated);
  SVal SizeOfPlace = getExtentSizeOfPlace(NE, C);
  const auto SizeOfTargetCI = SizeOfTarget.getAs<nonloc::ConcreteInt>();
  if (!SizeOfTargetCI)
    return true;
  const auto SizeOfPlaceCI = SizeOfPlace.getAs<nonloc::ConcreteInt>();
  if (!SizeOfPlaceCI)
    return true;

  if ((SizeOfPlaceCI->getValue() < SizeOfTargetCI->getValue()) ||
      (IsArrayTypeAllocated &&
       SizeOfPlaceCI->getValue() >= SizeOfTargetCI->getValue())) {
    if (ExplodedNode *N = C.generateErrorNode(C.getState())) {
      std::string Msg;
      // TODO: use clang constant
      if (IsArrayTypeAllocated &&
          SizeOfPlaceCI->getValue() > SizeOfTargetCI->getValue())
        Msg = std::string(llvm::formatv(
            "{0} bytes is possibly not enough for array allocation which "
            "requires {1} bytes. Current overhead requires the size of {2} "
            "bytes",
            SizeOfPlaceCI->getValue(), SizeOfTargetCI->getValue(),
            SizeOfPlaceCI->getValue() - SizeOfTargetCI->getValue()));
      else if (IsArrayTypeAllocated &&
               SizeOfPlaceCI->getValue() == SizeOfTargetCI->getValue())
        Msg = std::string(llvm::formatv(
            "Storage provided to placement new is only {0} bytes, "
            "whereas the allocated array type requires more space for "
            "internal needs",
            SizeOfPlaceCI->getValue(), SizeOfTargetCI->getValue()));
      else
        Msg = std::string(llvm::formatv(
            "Storage provided to placement new is only {0} bytes, "
            "whereas the allocated type requires {1} bytes",
            SizeOfPlaceCI->getValue(), SizeOfTargetCI->getValue()));

      auto R = std::make_unique<PathSensitiveBugReport>(SBT, Msg, N);
      bugreporter::trackExpressionValue(N, NE->getPlacementArg(0), *R);
      C.emitReport(std::move(R));

      return false;
    }
  }

  return true;
}

void PlacementNewChecker::emitBadAlignReport(const Expr *P, CheckerContext &C,
                                             unsigned AllocatedTAlign,
                                             unsigned StorageTAlign) const {
  ProgramStateRef State = C.getState();
  if (ExplodedNode *N = C.generateErrorNode(State)) {
    std::string Msg(llvm::formatv("Storage type is aligned to {0} bytes but "
                                  "allocated type is aligned to {1} bytes",
                                  StorageTAlign, AllocatedTAlign));

    auto R = std::make_unique<PathSensitiveBugReport>(ABT, Msg, N);
    bugreporter::trackExpressionValue(N, P, *R);
    C.emitReport(std::move(R));
  }
}

unsigned PlacementNewChecker::getStorageAlign(CheckerContext &C,
                                              const ValueDecl *VD) const {
  unsigned StorageTAlign = C.getASTContext().getTypeAlign(VD->getType());
  if (unsigned SpecifiedAlignment = VD->getMaxAlignment())
    StorageTAlign = SpecifiedAlignment;

  return StorageTAlign / C.getASTContext().getCharWidth();
}

void PlacementNewChecker::checkElementRegionAlign(
    const ElementRegion *R, CheckerContext &C, const Expr *P,
    unsigned AllocatedTAlign) const {
  auto IsBaseRegionAlignedProperly = [this, R, &C, P,
                                      AllocatedTAlign]() -> bool {
    // Unwind nested ElementRegion`s to get the type.
    const MemRegion *SuperRegion = R;
    while (true) {
      if (SuperRegion->getKind() == MemRegion::ElementRegionKind) {
        SuperRegion = cast<SubRegion>(SuperRegion)->getSuperRegion();
        continue;
      }

      break;
    }

    const DeclRegion *TheElementDeclRegion = SuperRegion->getAs<DeclRegion>();
    if (!TheElementDeclRegion)
      return false;

    const DeclRegion *BaseDeclRegion = R->getBaseRegion()->getAs<DeclRegion>();
    if (!BaseDeclRegion)
      return false;

    unsigned BaseRegionAlign = 0;
    // We must use alignment TheElementDeclRegion if it has its own alignment
    // specifier
    if (TheElementDeclRegion->getDecl()->getMaxAlignment())
      BaseRegionAlign = getStorageAlign(C, TheElementDeclRegion->getDecl());
    else
      BaseRegionAlign = getStorageAlign(C, BaseDeclRegion->getDecl());

    if (AllocatedTAlign > BaseRegionAlign) {
      emitBadAlignReport(P, C, AllocatedTAlign, BaseRegionAlign);
      return false;
    }

    return true;
  };

  auto CheckElementRegionOffset = [this, R, &C, P, AllocatedTAlign]() -> void {
    RegionOffset TheOffsetRegion = R->getAsOffset();
    if (TheOffsetRegion.hasSymbolicOffset())
      return;

    unsigned Offset =
        TheOffsetRegion.getOffset() / C.getASTContext().getCharWidth();
    unsigned AddressAlign = Offset % AllocatedTAlign;
    if (AddressAlign != 0) {
      emitBadAlignReport(P, C, AllocatedTAlign, AddressAlign);
      return;
    }
  };

  if (IsBaseRegionAlignedProperly()) {
    CheckElementRegionOffset();
  }
}

void PlacementNewChecker::checkFieldRegionAlign(
    const FieldRegion *R, CheckerContext &C, const Expr *P,
    unsigned AllocatedTAlign) const {
  const MemRegion *BaseRegion = R->getBaseRegion();
  if (!BaseRegion)
    return;

  if (const VarRegion *TheVarRegion = BaseRegion->getAs<VarRegion>()) {
    if (isVarRegionAlignedProperly(TheVarRegion, C, P, AllocatedTAlign)) {
      // We've checked type align but, unless FieldRegion
      // offset is zero, we also need to check its own
      // align.
      RegionOffset Offset = R->getAsOffset();
      if (Offset.hasSymbolicOffset())
        return;

      int64_t OffsetValue =
          Offset.getOffset() / C.getASTContext().getCharWidth();
      unsigned AddressAlign = OffsetValue % AllocatedTAlign;
      if (AddressAlign != 0)
        emitBadAlignReport(P, C, AllocatedTAlign, AddressAlign);
    }
  }
}

bool PlacementNewChecker::isVarRegionAlignedProperly(
    const VarRegion *R, CheckerContext &C, const Expr *P,
    unsigned AllocatedTAlign) const {
  const VarDecl *TheVarDecl = R->getDecl();
  unsigned StorageTAlign = getStorageAlign(C, TheVarDecl);
  if (AllocatedTAlign > StorageTAlign) {
    emitBadAlignReport(P, C, AllocatedTAlign, StorageTAlign);

    return false;
  }

  return true;
}

bool PlacementNewChecker::checkPlaceIsAlignedProperly(const CXXNewExpr *NE,
                                                      CheckerContext &C) const {
  const Expr *Place = NE->getPlacementArg(0);

  QualType AllocatedT = NE->getAllocatedType();
  unsigned AllocatedTAlign = C.getASTContext().getTypeAlign(AllocatedT) /
                             C.getASTContext().getCharWidth();

  SVal PlaceVal = C.getSVal(Place);
  if (const MemRegion *MRegion = PlaceVal.getAsRegion()) {
    if (const ElementRegion *TheElementRegion = MRegion->getAs<ElementRegion>())
      checkElementRegionAlign(TheElementRegion, C, Place, AllocatedTAlign);
    else if (const FieldRegion *TheFieldRegion = MRegion->getAs<FieldRegion>())
      checkFieldRegionAlign(TheFieldRegion, C, Place, AllocatedTAlign);
    else if (const VarRegion *TheVarRegion = MRegion->getAs<VarRegion>())
      isVarRegionAlignedProperly(TheVarRegion, C, Place, AllocatedTAlign);
  }

  return true;
}

void PlacementNewChecker::checkPreStmt(const CXXNewExpr *NE,
                                       CheckerContext &C) const {
  // Check only the default placement new.
  if (!NE->getOperatorNew()->isReservedGlobalPlacementOperator())
    return;

  if (NE->getNumPlacementArgs() == 0)
    return;

  if (!checkPlaceCapacityIsSufficient(NE, C))
    return;

  checkPlaceIsAlignedProperly(NE, C);
}

void ento::registerPlacementNewChecker(CheckerManager &mgr) {
  mgr.registerChecker<PlacementNewChecker>();
}

bool ento::shouldRegisterPlacementNewChecker(const CheckerManager &mgr) {
  return true;
}
