//=== ErrnoChecker.cpp ------------------------------------------*- 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 defines an "errno checker" that can detect some invalid use of the
// system-defined value 'errno'. This checker works together with the
// ErrnoModeling checker and other checkers like StdCLibraryFunctions.
//
//===----------------------------------------------------------------------===//

#include "ErrnoModeling.h"
#include "clang/AST/ParentMapContext.h"
#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"
#include "clang/StaticAnalyzer/Core/Checker.h"
#include "clang/StaticAnalyzer/Core/CheckerManager.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/SVals.h"
#include "llvm/ADT/STLExtras.h"
#include <optional>

using namespace clang;
using namespace ento;
using namespace errno_modeling;

namespace {

class ErrnoChecker
    : public Checker<check::Location, check::PreCall, check::RegionChanges> {
public:
  void checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
                     CheckerContext &) const;
  void checkPreCall(const CallEvent &Call, CheckerContext &C) const;
  ProgramStateRef
  checkRegionChanges(ProgramStateRef State,
                     const InvalidatedSymbols *Invalidated,
                     ArrayRef<const MemRegion *> ExplicitRegions,
                     ArrayRef<const MemRegion *> Regions,
                     const LocationContext *LCtx, const CallEvent *Call) const;

  /// Indicates if a read (load) of \c errno is allowed in a non-condition part
  /// of \c if, \c switch, loop and conditional statements when the errno
  /// value may be undefined.
  bool AllowErrnoReadOutsideConditions = true;

private:
  void generateErrnoNotCheckedBug(CheckerContext &C, ProgramStateRef State,
                                  const MemRegion *ErrnoRegion,
                                  const CallEvent *CallMayChangeErrno) const;

  BugType BT_InvalidErrnoRead{this, "Value of 'errno' could be undefined",
                              "Error handling"};
  BugType BT_ErrnoNotChecked{this, "Value of 'errno' was not checked",
                             "Error handling"};
};

} // namespace

static ProgramStateRef setErrnoStateIrrelevant(ProgramStateRef State) {
  return setErrnoState(State, Irrelevant);
}

/// Check if a statement (expression) or an ancestor of it is in a condition
/// part of a (conditional, loop, switch) statement.
static bool isInCondition(const Stmt *S, CheckerContext &C) {
  ParentMapContext &ParentCtx = C.getASTContext().getParentMapContext();
  bool CondFound = false;
  while (S && !CondFound) {
    const DynTypedNodeList Parents = ParentCtx.getParents(*S);
    if (Parents.empty())
      break;
    const auto *ParentS = Parents[0].get<Stmt>();
    if (!ParentS || isa<CallExpr>(ParentS))
      break;
    switch (ParentS->getStmtClass()) {
    case Expr::IfStmtClass:
      CondFound = (S == cast<IfStmt>(ParentS)->getCond());
      break;
    case Expr::ForStmtClass:
      CondFound = (S == cast<ForStmt>(ParentS)->getCond());
      break;
    case Expr::DoStmtClass:
      CondFound = (S == cast<DoStmt>(ParentS)->getCond());
      break;
    case Expr::WhileStmtClass:
      CondFound = (S == cast<WhileStmt>(ParentS)->getCond());
      break;
    case Expr::SwitchStmtClass:
      CondFound = (S == cast<SwitchStmt>(ParentS)->getCond());
      break;
    case Expr::ConditionalOperatorClass:
      CondFound = (S == cast<ConditionalOperator>(ParentS)->getCond());
      break;
    case Expr::BinaryConditionalOperatorClass:
      CondFound = (S == cast<BinaryConditionalOperator>(ParentS)->getCommon());
      break;
    default:
      break;
    }
    S = ParentS;
  }
  return CondFound;
}

void ErrnoChecker::generateErrnoNotCheckedBug(
    CheckerContext &C, ProgramStateRef State, const MemRegion *ErrnoRegion,
    const CallEvent *CallMayChangeErrno) const {
  if (ExplodedNode *N = C.generateNonFatalErrorNode(State)) {
    SmallString<100> StrBuf;
    llvm::raw_svector_ostream OS(StrBuf);
    if (CallMayChangeErrno) {
      OS << "Value of 'errno' was not checked and may be overwritten by "
            "function '";
      const auto *CallD =
          dyn_cast_or_null<FunctionDecl>(CallMayChangeErrno->getDecl());
      assert(CallD && CallD->getIdentifier());
      OS << CallD->getIdentifier()->getName() << "'";
    } else {
      OS << "Value of 'errno' was not checked and is overwritten here";
    }
    auto BR = std::make_unique<PathSensitiveBugReport>(BT_ErrnoNotChecked,
                                                       OS.str(), N);
    BR->markInteresting(ErrnoRegion);
    C.emitReport(std::move(BR));
  }
}

void ErrnoChecker::checkLocation(SVal Loc, bool IsLoad, const Stmt *S,
                                 CheckerContext &C) const {
  std::optional<ento::Loc> ErrnoLoc = getErrnoLoc(C.getState());
  if (!ErrnoLoc)
    return;

  auto L = Loc.getAs<ento::Loc>();
  if (!L || *ErrnoLoc != *L)
    return;

  ProgramStateRef State = C.getState();
  ErrnoCheckState EState = getErrnoState(State);

  if (IsLoad) {
    switch (EState) {
    case MustNotBeChecked:
      // Read of 'errno' when it may have undefined value.
      if (!AllowErrnoReadOutsideConditions || isInCondition(S, C)) {
        if (ExplodedNode *N = C.generateErrorNode()) {
          auto BR = std::make_unique<PathSensitiveBugReport>(
              BT_InvalidErrnoRead,
              "An undefined value may be read from 'errno'", N);
          BR->markInteresting(ErrnoLoc->getAsRegion());
          C.emitReport(std::move(BR));
        }
      }
      break;
    case MustBeChecked:
      // 'errno' has to be checked. A load is required for this, with no more
      // information we can assume that it is checked somehow.
      // After this place 'errno' is allowed to be read and written.
      State = setErrnoStateIrrelevant(State);
      C.addTransition(State);
      break;
    default:
      break;
    }
  } else {
    switch (EState) {
    case MustBeChecked:
      // 'errno' is overwritten without a read before but it should have been
      // checked.
      generateErrnoNotCheckedBug(C, setErrnoStateIrrelevant(State),
                                 ErrnoLoc->getAsRegion(), nullptr);
      break;
    case MustNotBeChecked:
      // Write to 'errno' when it is not allowed to be read.
      // After this place 'errno' is allowed to be read and written.
      State = setErrnoStateIrrelevant(State);
      C.addTransition(State);
      break;
    default:
      break;
    }
  }
}

void ErrnoChecker::checkPreCall(const CallEvent &Call,
                                CheckerContext &C) const {
  const auto *CallF = dyn_cast_or_null<FunctionDecl>(Call.getDecl());
  if (!CallF)
    return;

  CallF = CallF->getCanonicalDecl();
  // If 'errno' must be checked, it should be done as soon as possible, and
  // before any other call to a system function (something in a system header).
  // To avoid use of a long list of functions that may change 'errno'
  // (which may be different with standard library versions) assume that any
  // function can change it.
  // A list of special functions can be used that are allowed here without
  // generation of diagnostic. For now the only such case is 'errno' itself.
  // Probably 'strerror'?
  if (CallF->isExternC() && CallF->isGlobal() &&
      C.getSourceManager().isInSystemHeader(CallF->getLocation()) &&
      !isErrnoLocationCall(Call)) {
    if (getErrnoState(C.getState()) == MustBeChecked) {
      std::optional<ento::Loc> ErrnoLoc = getErrnoLoc(C.getState());
      assert(ErrnoLoc && "ErrnoLoc should exist if an errno state is set.");
      generateErrnoNotCheckedBug(C, setErrnoStateIrrelevant(C.getState()),
                                 ErrnoLoc->getAsRegion(), &Call);
    }
  }
}

ProgramStateRef ErrnoChecker::checkRegionChanges(
    ProgramStateRef State, const InvalidatedSymbols *Invalidated,
    ArrayRef<const MemRegion *> ExplicitRegions,
    ArrayRef<const MemRegion *> Regions, const LocationContext *LCtx,
    const CallEvent *Call) const {
  std::optional<ento::Loc> ErrnoLoc = getErrnoLoc(State);
  if (!ErrnoLoc)
    return State;
  const MemRegion *ErrnoRegion = ErrnoLoc->getAsRegion();

  // If 'errno' is invalidated we can not know if it is checked or written into,
  // allow read and write without bug reports.
  if (llvm::is_contained(Regions, ErrnoRegion))
    return clearErrnoState(State);

  // Always reset errno state when the system memory space is invalidated.
  // The ErrnoRegion is not always found in the list in this case.
  if (llvm::is_contained(Regions, ErrnoRegion->getMemorySpace(State)))
    return clearErrnoState(State);

  return State;
}

void ento::registerErrnoChecker(CheckerManager &mgr) {
  const AnalyzerOptions &Opts = mgr.getAnalyzerOptions();
  auto *Checker = mgr.registerChecker<ErrnoChecker>();
  Checker->AllowErrnoReadOutsideConditions = Opts.getCheckerBooleanOption(
      Checker, "AllowErrnoReadOutsideConditionExpressions");
}

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