higuoxing

higuoxing

github
twitter

第一次嘗試撰寫 Clang 插件

為什麼我想寫一個 Clang 插件?#

我每天的工作是為一個名為 Greenplum 的資料庫開發擴展。這是一個源自 PostgreSQL 的分散式資料庫。每次我使用它時,當遇到使用 PG_TRY()/PG_CATCH() 區塊的 PostgreSQL 錯誤處理代碼時,我都感到警惕,因為我們見過許多因為誤用而導致的錯誤。我決定編寫一些自動化工具來捕捉這些錯誤。

PostgreSQL 錯誤處理代碼是什麼樣的?#

除了 PG_TRY()PG_CATCH(),在 PostgreSQL 錯誤處理過程中還涉及 2 個額外的宏:ereport()PG_END_TRY()

  • PG_TRY()PG_CATCH()PG_END_TRY() 用於構建錯誤處理控制流。
  • ereport() 用於報告錯誤。

PostgreSQL 中錯誤處理過程的代碼模式如下所示,

PG_TRY();
{
  // FallibleMethod() 包含潛在的錯誤報告調用,例如,
  // ereport();
  FallibleMethod();
}
PG_CATCH();
{
  // 執行錯誤處理。
}
PG_END_TRY();

這些宏的代碼模式看起來與其他語言中的 try-catch 表達式非常相似,但它們的用法更為複雜。這些宏的簡化定義如下,

// 我已經註釋掉了宏定義關鍵字,以便我們可以受益於
//   語法高亮 :-)
// 如果你想找到這些宏的完整定義,可以在下面的鏈接中找到
//   https://github.com/postgres/postgres/blob/f7431bca8b0138bdbce7025871560d39119565a0/src/include/utils/elog.h#L384

// #define PG_TRY()
  do {
    sigjmp_buf *save_exception_stack = PG_exception_stack;
    sigjmp_buf local_sigjmp_buf;
    if (sigsetjmp(local_sigjmp_buf, 0) == 0)
    {
      PG_exception_stack = &local_sigjmp_buf

// #define PG_CATCH()
    }
    else
    {
      PG_exception_stack = save_exception_stack;

// #define PG_END_TRY()
    }
    PG_exception_stack = save_exception_stack;
  } while (0)
  
// #define ereport()
  if (PG_exception_stack != NULL)
    siglongjmp(*PG_exception_stack, 1);
  else
  {
    // 在現實中,我們不希望這個分支被執行。
    ...
  }

PG_TRY()PG_CATCH() 是如何工作的?#

全局變量 sigjmp_buf *PG_exception_stack 保存了前一次 sigsetjmp() 調用的環境(堆棧上下文)。在進入可失敗的代碼區域(由 PG_TRY()PG_CATCH() 包裝)之前,我們使用 sigjmp_buf *save_exception_stack 保存先前的環境,並將當前環境分配給 PG_exception_stack,這樣如果發生任何錯誤(調用 ereport()),我們可以使用 siglongjmp(*PG_exception_stack) 跳轉到正確的位置(被正確的 PG_CATCH() 區塊捕獲)。

PG_CATCH() 區塊中,我們在採取任何錯誤處理行動之前恢復 PG_exception_stack 的先前環境,因此如果我們想將錯誤傳遞給上層調用者(調用 PG_RE_THROW(),這是 siglongjmp() 的另一種包裝),siglongjmp(*PG_exception_stack) 可以再次跳轉到正確的位置。

問題#

常見的錯誤之一是在 PG_TRY() 區塊內使用跳轉語句(例如 returnbreakcontinuegoto),即使對於經驗豐富的 PostgreSQL 貢獻者也是如此1。例如,如果我們在 PG_TRY() 區塊內使用 return 語句,則在離開 PG_TRY() 區塊之前,PG_exception_stack 不會恢復到正確的堆棧上下文,這可能會對 PostgreSQL/Greenplum 伺服器造成嚴重問題,例如伺服器崩潰2

我們試圖檢測的代碼模式是在 PG_TRY() 區塊內使用不安全的 returnbreakcontinuegoto 語句。不安全的代碼模式可以總結為以下規則,

  • PG_TRY() 區塊的任何地方使用 return 語句。

    PG_TRY();
    {
      ...
      // 不安全,因為 PG_exception_stack 在離開 PG_TRY-PG_CATCH
      // 區塊後仍然存儲當前環境。
      return;
    }
    PG_CATCH();
    {
      ...
    }
    PG_END_TRY();
    
  • PG_TRY() 區塊的任何地方使用 break 語句,除了在 switchdo-whilewhilefor 語句內使用的情況。

    PG_TRY();
    {
      ...
      // 不安全,因為 break 語句跳出 PG_TRY() 區塊,這會破壞錯誤處理
      // 堆棧上下文或環境。
      break;
      do
      {
        // 安全,因為我們跳出的是 do-while 循環而不是
        // PG_TRY() 區塊。
        break;
      } while (0);
      while (1)
      {
        // 安全。同上。
        break;
      }
      for (;;)
      {
        // 安全。同上。
      }
      switch (c)
      {
      case 1:
        // 安全。同上。
        break;
      }
    }
    PG_CATCH();
    {
      ...
    }
    PG_END_TRY();
    
  • PG_TRY() 區塊的任何地方使用 continue 語句,除了在 do-whilewhilefor 循環內使用的情況。

    PG_TRY();
    {
      // 不安全,因為 continue 語句將終止從 PG_TRY() 宏擴展的 do-while 循環
      // 並跳出 PG_TRY() 區塊,導致錯誤處理堆棧上下文破壞。
      continue;
      do {
        // 安全。
        continue;
      } while (0);
      while (0)
      {
        // 安全。
        continue;
      }
      for (;;)
      {
        // 安全。
        continue;
      }
    }
    PG_CATCH();
    {
      ...
    }
    PG_END_TRY();
    
  • PG_TRY() 區塊外有標籤的 goto 語句。

    label1:
      PG_TRY();
      {
      label2:
        // 不安全。因為我們跳出 PG_TRY() 區塊,這會破壞堆棧上下文。
        goto label1;
        // 安全。
        goto label2;
      }
      PG_CATCH();
      {
        ...
      }
      PG_END_TRY();
    

AST 匹配器還是靜態分析器?#

PG_TRY() 是 C 中的一個宏,並且它總是擴展為相同的東西。此外,我們想檢測的語句非常簡單,不涉及跟踪符號狀態的變化。Clang 的 AST 匹配器對於我們的問題已經足夠好。

首先,我們註冊一個回調函數 checkEndOfTranslationUnit() 來找出包含 return/break/continue/goto 語句的 PG_TRY() 區塊。該回調函數將在編譯期間對每個翻譯單元進行調用。當一個 PG_TRY() 區塊被匹配時,我們將仔細檢查它是否真的不安全,以減少誤報。帶有註釋的代碼片段如下所示。

class ReturnInPgTryBlockChecker : public Checker<check::EndOfTranslationUnit> {
public:
  void checkEndOfTranslationUnit(const TranslationUnitDecl *TU,
                                 AnalysisManager &AM, BugReporter &B) const {
    MatchFinder F;
    PgTryBlockMatcherCallback CB;
    StatementMatcher PgTry =
        // PG_TRY() 將擴展為以下表達式。
        // if (__sigsetjmp() == 0) {
        //   PG_exception_stack = &local_sigjmp_buf;
        //   ...
        // }
        ifStmt(
            // 'if' 語句必須包含一個二元運算符,且該二元運算符
            // 必須是 '=='。
            hasCondition(
                binaryOperator(allOf(hasOperatorName("=="),
                                     // '==' 的一個操作數必須是函數調用,且該
                                     // 函數必須名為 '__sigsetjmp'。
                                     // 另一個操作數必須是整數字面量 '0'。
                                     hasOperands(callExpr(callee(functionDecl(
                                                     hasName("__sigsetjmp")))),
                                                 integerLiteral(equals(0)))))),
            // 'if' 語句必須有一個 'then' 區塊,且 'then' 區塊必須
            // 包含 'return'、'break'、'continue' 和 'goto' 語句之一。
            hasThen(eachOf(
                // 為了方便,我們將 PG_TRY() 區塊與 return 語句綁定
                // 名稱 'ReturnInPgTryBlock',以便我們可以立即發出警告消息
                // 之後。
                forEachDescendant(returnStmt().bind("ReturnInPgTryBlock")),
                anyOf(hasDescendant(breakStmt()), hasDescendant(continueStmt()),
                      hasDescendant(gotoStmt())))))
            // 我們將感興趣的 PG_TRY() 區塊的 AST 綁定到名稱 'PgTryBlock' 以便稍後仔細檢查。
            .bind("PgTryBlock");

    // &CB 是稍後將被調用的回調,用於仔細檢查匹配的
    // PG_TRY() 區塊的 AST。
    F.addMatcher(PgTry, &CB);
    // 匹配 AST!
    F.matchAST(TU->getASTContext());
  }
};

然後,我們仔細檢查匹配的 PG_TRY() 區塊的 AST。當綁定到名稱 "ReturnInPgTryBlock""PgTryBlock" 的 AST 被匹配時,以下回調將被調用。

class PgTryBlockMatcherCallback : public MatchFinder::MatchCallback {
public:
  PgTryBlockMatcherCallback() = default;

  void run(const MatchFinder::MatchResult &Result) override {
    ASTContext *Ctx = Result.Context;

    if (const ReturnStmt *Return =
            Result.Nodes.getNodeAs<ReturnStmt>("ReturnInPgTryBlock")) {
      // 我們在 PG_TRY 區塊內找到了 return 語句。讓我們發出警告
      // 關於它。
      DiagnosticsEngine &DE = Ctx->getDiagnostics();
      unsigned DiagID = DE.getCustomDiagID(
          DiagnosticsEngine::Error,
          "在 PG_TRY 區塊內使用不安全的 return 語句");
      auto DB = DE.Report(Return->getReturnLoc(), DiagID);
      DB.AddSourceRange(
          CharSourceRange::getCharRange(Return->getSourceRange()));
    } else if (const IfStmt *If =
                   Result.Nodes.getNodeAs<IfStmt>("PgTryBlock")) {
      // 檢查 PG_TRY() 區塊內的 'break'/'continue'/'goto' 語句是否不安全。
      const Stmt *Then = If->getThen();
      CheckUnsafeBreakStmt(Then, Ctx);
      CheckUnsafeContinueStmt(Then, Ctx);
      CheckUnsafeGotoStmt(Then, Ctx);
    }
  }
};

檢查在 PG_TRY() 區塊內使用 break/continue/goto 語句的安全性的代碼非常相似。這裡,我們以 CheckUnsafeBreakStmt() 為例。其基本思想是在匹配的 AST 上執行 BFS。

static void CheckUnsafeBreakStmt(const Stmt *Then, ASTContext *Ctx) {
  std::queue<const Stmt *> StmtQueue;
  StmtQueue.push(Then);
  while (!StmtQueue.empty()) {
    const Stmt *CurrStmt = StmtQueue.front();
    StmtQueue.pop();

    if (!CurrStmt)
      continue;

    if (const BreakStmt *Break =
            llvm::dyn_cast_if_present<BreakStmt>(CurrStmt)) {
      // 我們在 PG_TRY 區塊內找到了 break 語句。讓我們發出警告
      // 關於它。
      DiagnosticsEngine &DE = Ctx->getDiagnostics();
      unsigned DiagID = DE.getCustomDiagID(
          DiagnosticsEngine::Error,
          "在 PG_TRY 區塊內使用的 break 語句是不安全的");
      auto DB = DE.Report(Break->getBreakLoc(), DiagID);
      DB.AddSourceRange(CharSourceRange::getCharRange(Break->getSourceRange()));
    }

    // 在 while/do-while/for/switch 語句中的 break 語句是安全的。我們不
    // 需要對子節點執行 BFS。
    if (llvm::isa<WhileStmt>(CurrStmt) || llvm::isa<DoStmt>(CurrStmt) ||
        llvm::isa<ForStmt>(CurrStmt) || llvm::isa<SwitchStmt>(CurrStmt)) {
      continue;
    }

    for (const Stmt *C : CurrStmt->children()) {
      StmtQueue.push(C);
    }
  }
}

現在,我們的檢查器可以報告 PostgreSQL 基於項目中的不安全代碼模式。檢查器的源代碼可以在我的 GitHub 倉庫中找到3

它在現實世界中找到任何潛在的錯誤嗎?#

是的,它找到了!我在 PostgreSQL1 中發現了一些不安全的代碼,當然也在 Greenplum 中(我沒有向 Greenplum 提交問題,因為我想先在 PostgreSQL 中修復它,然後再將補丁選擇性地回滾到 Greenplum)。我從 pgsql-hackers 郵件列表中得到的一些有趣的回覆如下,

  • Tom Lane 提到在 PG_TRY() 區塊內使用 break/continue/goto 也可能會搞砸事情。
  • Andres Freund 提供了一個非常酷的編譯器黑客技術,使用 clang 的線程安全分析。該補丁似乎比我的 AST 匹配器想法更好。如果他的補丁被提交,我們可以在編譯 PostgreSQL 時拒絕這種不安全的代碼模式。
From d1c99e9d12ba01adb21c5f17c792be44cfeef20f Mon Sep 17 00:00:00 2001
From: Andres Freund <[email protected]>
Date: Thu, 12 Jan 2023 21:18:55 -0800
Subject: [PATCH v1] wip: use clang anotations to warn if code in
 PG_TRY/CATCH/FINALLY returns

Only hooked up to meson right now.
---
 meson.build              |  1 +
 src/include/utils/elog.h | 43 +++++++++++++++++++++++++++++++++++++---
 2 files changed, 41 insertions(+), 3 deletions(-)

diff --git a/meson.build b/meson.build
index 45fb9dd616e..66a40e728f4 100644
--- a/meson.build
+++ b/meson.build
@@ -1741,6 +1741,7 @@ common_warning_flags = [
   '-Wimplicit-fallthrough=3',
   '-Wcast-function-type',
   '-Wshadow=compatible-local',
+  '-Wthread-safety',
   # This was included in -Wall/-Wformat in older GCC versions
   '-Wformat-security',
 ]
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index 4a9562fdaae..b211e08322a 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -381,32 +381,69 @@ extern PGDLLIMPORT ErrorContextCallback *error_context_stack;
  * same within each component macro of the given PG_TRY() statement.
  *----------
  */
+
+
+/*
+ * Annotations for detecting returns inside a PG_TRY(), using clang's thread
+ * safety annotations.
+ *
+ * The "lock" implementations need no_thread_safety_analysis as clang can't
+ * understand how a lock is implemented. We wouldn't want an implementation
+ * anyway, since there's no real lock here.
+ */
+#ifdef __clang__
+
+typedef int __attribute__((capability("no_returns_in_pg_try"))) no_returns_handle_t;
+
+static inline void no_returns_start(no_returns_handle_t l)
+	__attribute__((acquire_capability(l)))
+	__attribute__((no_thread_safety_analysis))
+{
+}
+
+static inline void no_returns_stop(no_returns_handle_t l)
+	__attribute__((release_capability(l)))
+	__attribute__((no_thread_safety_analysis))
+{}
+#else
+typedef int pg_attribute_unused() no_returns_handle_t;
+#define no_returns_start(t) (void)0
+#define no_returns_stop(t) (void)0
+#endif
+
 #define PG_TRY(...)  \
 	do { \
 		sigjmp_buf *_save_exception_stack##__VA_ARGS__ = PG_exception_stack; \
 		ErrorContextCallback *_save_context_stack##__VA_ARGS__ = error_context_stack; \
 		sigjmp_buf _local_sigjmp_buf##__VA_ARGS__; \
 		bool _do_rethrow##__VA_ARGS__ = false; \
+		no_returns_handle_t no_returns_handle##__VA_ARGS__ = 0; \
 		if (sigsetjmp(_local_sigjmp_buf##__VA_ARGS__, 0) == 0) \
 		{ \
-			PG_exception_stack = &_local_sigjmp_buf##__VA_ARGS__
+			PG_exception_stack = &_local_sigjmp_buf##__VA_ARGS__; \
+		    no_returns_start(no_returns_handle##__VA_ARGS__)
 
 #define PG_CATCH(...)	\
+			no_returns_stop(no_returns_handle##__VA_ARGS__); \
 		} \
 		else \
 		{ \
 			PG_exception_stack = _save_exception_stack##__VA_ARGS__; \
-			error_context_stack = _save_context_stack##__VA_ARGS__
+			error_context_stack = _save_context_stack##__VA_ARGS__; \
+		    no_returns_start(no_returns_handle##__VA_ARGS__)
 
 #define PG_FINALLY(...) \
+			no_returns_stop(no_returns_handle##__VA_ARGS__); \
 		} \
 		else \
 			_do_rethrow##__VA_ARGS__ = true; \
 		{ \
 			PG_exception_stack = _save_exception_stack##__VA_ARGS__; \
-			error_context_stack = _save_context_stack##__VA_ARGS__
+			error_context_stack = _save_context_stack##__VA_ARGS__; \
+		    no_returns_start(no_returns_handle##__VA_ARGS__)
 
 #define PG_END_TRY(...)  \
+			no_returns_stop(no_returns_handle##__VA_ARGS__); \
 		} \
 		if (_do_rethrow##__VA_ARGS__) \
 				PG_RE_THROW(); \
-- 
2.38.0

下一步是什麼?#

這是我第一次嘗試編寫基於 Clang 的檢查器。除了在 PG_TRY() 區塊內使用不安全的 return/break/continue/goto 語句外,仍然存在一些不安全的代碼模式,例如,在 PG_TRY() 區塊內修改自動存儲類的局部變量並在 PG_CATCH() 區塊中使用它。未來能夠有更多檢查器來檢查這些不安全的代碼模式將是非常好的。

Footnotes#

  1. https://www.postgresql.org/message-id/CACpMh+CMsGMRKFzFMm3bYTzQmMU5nfEEoEDU2apJcc4hid36AQ@mail.gmail.com 2

  2. https://github.com/greenplum-db/gpdb/pull/14205

  3. https://github.com/higuoxing/clang-plugins/blob/88eeb2bea0ade224807bb3e35f1d048dd4d3697c/lib/ReturnInPgTryBlockChecker.cpp

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。