[analyzer] MIGChecker: A checker for Mach Interface Generator conventions.
authorArtem Dergachev <artem.dergachev@gmail.com>
Thu, 21 Feb 2019 23:55:28 +0000 (23:55 +0000)
committerArtem Dergachev <artem.dergachev@gmail.com>
Thu, 21 Feb 2019 23:55:28 +0000 (23:55 +0000)
This checker detects use-after-free bugs in (various forks of) the Mach kernel
that are caused by errors in MIG server routines - functions called remotely by
MIG clients. The MIG convention forces the server to only deallocate objects
it receives from the client when the routine is executed successfully.
Otherwise, if the server routine exits with an error, the client assumes that
it needs to deallocate the out-of-line data it passed to the server manually.
This means that deallocating such data within the MIG routine and then returning
a non-zero error code is always a dangerous use-after-free bug.

rdar://problem/35380337

Differential Revision: https://reviews.llvm.org/D57558

llvm-svn: 354635

clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
clang/lib/StaticAnalyzer/Checkers/MIGChecker.cpp [new file with mode: 0644]
clang/test/Analysis/mig.mm [new file with mode: 0644]

index 0f7ed61..bf015c5 100644 (file)
@@ -828,6 +828,15 @@ def OSObjectCStyleCast : Checker<"OSObjectCStyleCast">,
 
 } // end "optin.osx"
 
+let ParentPackage = OSXAlpha in {
+
+def MIGChecker : Checker<"MIG">,
+  HelpText<"Find violations of the Mach Interface Generator "
+           "calling convention">,
+  Documentation<NotDocumented>;
+
+} // end "alpha.osx"
+
 let ParentPackage = CocoaAlpha in {
 
 def IvarInvalidationModeling : Checker<"IvarInvalidationModeling">,
index da26abe..db2d0e7 100644 (file)
@@ -51,6 +51,7 @@ add_clang_library(clangStaticAnalyzerCheckers
   MallocOverflowSecurityChecker.cpp
   MallocSizeofChecker.cpp
   MmapWriteExecChecker.cpp
+  MIGChecker.cpp
   MoveChecker.cpp
   MPI-Checker/MPIBugReporter.cpp
   MPI-Checker/MPIChecker.cpp
diff --git a/clang/lib/StaticAnalyzer/Checkers/MIGChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/MIGChecker.cpp
new file mode 100644 (file)
index 0000000..8586d95
--- /dev/null
@@ -0,0 +1,144 @@
+//== MIGChecker.cpp - MIG calling convention checker ------------*- 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 MIGChecker, a Mach Interface Generator calling convention
+// checker. Namely, in MIG callback implementation the following rules apply:
+// - When a server routine returns KERN_SUCCESS, it must take ownership of
+//   resources (and eventually release them).
+// - Additionally, when returning KERN_SUCCESS, all out-parameters must be
+//   initialized.
+// - When it returns anything except KERN_SUCCESS it must not take ownership,
+//   because the message and its descriptors will be destroyed by the server
+//   function.
+// For now we only check the last rule, as its violations lead to dangerous
+// use-after-free exploits.
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"
+#include "clang/StaticAnalyzer/Core/BugReporter/BugType.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"
+
+using namespace clang;
+using namespace ento;
+
+namespace {
+class MIGChecker : public Checker<check::PostCall, check::PreStmt<ReturnStmt>> {
+  BugType BT{this, "Use-after-free (MIG calling convention violation)",
+             categories::MemoryError};
+
+  CallDescription vm_deallocate { "vm_deallocate", 3 };
+
+public:
+  void checkPostCall(const CallEvent &Call, CheckerContext &C) const;
+  void checkPreStmt(const ReturnStmt *RS, CheckerContext &C) const;
+};
+} // end anonymous namespace
+
+REGISTER_TRAIT_WITH_PROGRAMSTATE(ReleasedParameter, bool)
+
+static bool isCurrentArgSVal(SVal V, CheckerContext &C) {
+  SymbolRef Sym = V.getAsSymbol();
+  if (!Sym)
+    return false;
+
+  const auto *VR = dyn_cast_or_null<VarRegion>(Sym->getOriginRegion());
+  return VR && VR->hasStackParametersStorage() &&
+         VR->getStackFrame()->inTopFrame();
+}
+
+// This function will probably be replaced with looking up annotations.
+static bool isInMIGCall(const LocationContext *LC) {
+  const StackFrameContext *SFC;
+  // Find the top frame.
+  while (LC) {
+    SFC = LC->getStackFrame();
+    LC = SFC->getParent();
+  }
+
+  const auto *FD = dyn_cast<FunctionDecl>(SFC->getDecl());
+  if (!FD)
+    return false;
+
+  // FIXME: This is an unreliable (even if surprisingly reliable) heuristic.
+  // The real solution here is to make MIG annotate its callbacks in
+  // autogenerated headers so that we didn't need to think hard if it's
+  // actually a MIG callback.
+  QualType T = FD->getReturnType();
+  return T.getCanonicalType()->isIntegerType() &&
+         T.getAsString() == "kern_return_t";
+}
+
+void MIGChecker::checkPostCall(const CallEvent &Call, CheckerContext &C) const {
+  if (!isInMIGCall(C.getStackFrame()))
+    return;
+
+  if (!Call.isGlobalCFunction())
+    return;
+
+  if (!Call.isCalled(vm_deallocate))
+    return;
+
+  // TODO: Unhardcode "1".
+  SVal Arg = Call.getArgSVal(1);
+  if (isCurrentArgSVal(Arg, C))
+    C.addTransition(C.getState()->set<ReleasedParameter>(true));
+}
+
+void MIGChecker::checkPreStmt(const ReturnStmt *RS, CheckerContext &C) const {
+  // It is very unlikely that a MIG callback will be called from anywhere
+  // within the project under analysis and the caller isn't itself a routine
+  // that follows the MIG calling convention. Therefore we're safe to believe
+  // that it's always the top frame that is of interest. There's a slight chance
+  // that the user would want to enforce the MIG calling convention upon
+  // a random routine in the middle of nowhere, but given that the convention is
+  // fairly weird and hard to follow in the first place, there's relatively
+  // little motivation to spread it this way.
+  if (!C.inTopFrame())
+    return;
+
+  if (!isInMIGCall(C.getStackFrame()))
+    return;
+
+  // We know that the function is non-void, but what if the return statement
+  // is not there in the code? It's not a compile error, we should not crash.
+  if (!RS)
+    return;
+
+  ProgramStateRef State = C.getState();
+  if (!State->get<ReleasedParameter>())
+    return;
+
+  SVal V = C.getSVal(RS);
+  if (!State->isNonNull(V).isConstrainedTrue())
+    return;
+
+  ExplodedNode *N = C.generateErrorNode();
+  if (!N)
+    return;
+
+  auto R = llvm::make_unique<BugReport>(
+      BT,
+      "MIG callback fails with error after deallocating argument value. "
+      "This is a use-after-free vulnerability because the caller will try to "
+      "deallocate it again",
+      N);
+
+  C.emitReport(std::move(R));
+}
+
+void ento::registerMIGChecker(CheckerManager &Mgr) {
+  Mgr.registerChecker<MIGChecker>();
+}
+
+bool ento::shouldRegisterMIGChecker(const LangOptions &LO) {
+  return true;
+}
diff --git a/clang/test/Analysis/mig.mm b/clang/test/Analysis/mig.mm
new file mode 100644 (file)
index 0000000..1def03c
--- /dev/null
@@ -0,0 +1,36 @@
+// RUN: %clang_analyze_cc1 -w -analyzer-checker=core,alpha.osx.MIG -verify %s
+
+// XNU APIs.
+
+typedef int kern_return_t;
+#define KERN_SUCCESS 0
+#define KERN_ERROR 1
+
+typedef unsigned mach_port_name_t;
+typedef unsigned vm_address_t;
+typedef unsigned vm_size_t;
+
+kern_return_t vm_deallocate(mach_port_name_t, vm_address_t, vm_size_t);
+
+// Tests.
+
+kern_return_t basic_test(mach_port_name_t port, vm_address_t address, vm_size_t size) {
+  vm_deallocate(port, address, size);
+  if (size > 10) {
+    return KERN_ERROR; // expected-warning{{MIG callback fails with error after deallocating argument value. This is a use-after-free vulnerability because the caller will try to deallocate it again}}
+  }
+  return KERN_SUCCESS;
+}
+
+kern_return_t test_unknown_return_value(mach_port_name_t port, vm_address_t address, vm_size_t size) {
+  extern kern_return_t foo();
+
+  vm_deallocate(port, address, size);
+  // We don't know if it's a success or a failure.
+  return foo(); // no-warning
+}
+
+// Make sure we don't crash when they forgot to write the return statement.
+kern_return_t no_crash(mach_port_name_t port, vm_address_t address, vm_size_t size) {
+  vm_deallocate(port, address, size);
+}