Invalid Parameter Handler and Security Enhancements in the CRT

在 VC++ 的 CRT 中,除了標準的 CRT 之外,Microsoft 也加入安全性的檢查和報錯機制,許多原有的 functions 都被加上 _s 的後贅字。舉例來說:strcpy_s(), wcscpy_s(), _mbscpy_s() 各別是 strcpy(), wcscpy(), mbscpy() 的 security enhanced 版。這些 Security enhancement 的重點在於:

  1. Parameter validation
  2. Sized buffers
  3. Null termination
  4. Enhanced error reporting
  5. Filesystem security
  6. Windows security
  7. Format string syntax checking

針對上述項目的檢查與 enforcement ,security-enhanced CRT 一旦發現問題,是不會嘗試去修正或繞過的,錯了就是錯了,舉例來說:wcscpy_s() 在 copy 記憶體時,一旦發現 destination buffer 比 source buffer 小,它並不會自動進行 truncation ,參考下面這個從 MSDN 來的例子:

errno_t wcscpy_s(
   wchar_t *strDestination,
   size_t sizeInWords,
   const wchar_t *strSource 
);
strDestination sizeInBytes, sizeInWords strSource Return value Contents of strDestination
NULL any any EINVAL not modified
any any NULL EINVAL not modified
any 0, or too small any ERANGE not modified

從表中可以看到,一旦 destination 和 source 有不正確的配對,那麼就不會對 destination 進行動,自然也就沒有把字串 truncate 後複製過去的結果發生。因此如果 truncation 是期望的結局,那就必須改用 strncpy_s(), _strncpy_s_l(), wcsncpy_s(), _wcsncpy_s_l(), _mbsncpy_s(), _mbsncpy_s_l() 這類原本就有 truncate 語意的函式。所以了,引用一下 MSDN 總結:

... the secure functions do not prevent or correct security errors; rather, they catch errors when they occur. They perform additional checks for error conditions, and in the case of an error, they invoke an error handler (see Parameter Validation).

沒錯,security enhancement 強調的是參數使用前的檢查以及可錯誤發生後的處理,不是幫忙修正錯誤。

Parameter Validation

Parameter validation 機制會檢查傳入 _s functions 的參數,檢查包括:

  1. Checking pointers for NULL
  2. Checking that integers fall into a valid range
  3. Checking that enumeration values are valid
  4. Validate that the buffer is large enough before writing to it

當 CRT 發現 invalid parameter 時,它會呼叫 invalid parameter handler 來處理。 CRT 有預設的 invalid parameter handler ,它會呼叫 Dr. Watson 產生 dump ,然後跳出一個 Error report 程式詢問使用者要不要上傳這個錯誤報告。而程式若是以 debug mode 編譯,則是會跳出 assertion。

drwtsn32

Fig 1. Dr. Watson (不是這個華生?也不是裘德洛?)

WER

Fig 2. Windows Error Reporting 對話方塊

AssertionFig 3. Debug Mode 下跳出的 assertion failed 視窗

Where have all the Exceptions gone?

const wchar_t*  src = L"Hello";
wchar_t         dest[ 5 ];

__try {
    wcscpy_s( dest, _countof( dest ), src );
}
__except ( EXCEPTION_EXECUTE_HANDLER ) {
    // Error handle...
}

在某些程式裡頭會看到像上面那樣的片段程式碼,看起來似乎四平八穩:我們指定了 dest 和其大小,若是 src 長度超過了 dest , wcscpy_s() 透過 exception 報錯,我們就可以透過 SEH 來處理。但程式一執行,ㄜ,好像 crash 了… 不過現在我們已經可以可能的問題是什麼:若是程式沒有置換過 invalid parameter handler ,那麼是丟不出 exception 的,程式的 crash 便是可預期的結果。

Install my own Invalid Parameter Handler

/*!
 * \param[in] expression argument expression
 * \param[in] function   the name of the CRT function that received the invalid argument
 * \param[in] file       file name in the CRT source
 * \param[in] line       line in file
 * \param[in] pReserved  reserved
 */
void _invalid_parameter(
    const wchar_t* expression,
    const wchar_t* function, 
    const wchar_t* file, 
    unsigned int   line,
    uintptr_t      pReserved
);

上述是 invalid parameter handler 的 prototype。我們很容易仿造一個自己的,延續之前 wcscpy_s() 的例子,我們就可以實作一個會丟出 exception 的 handler:

void myInvalidParameterHandler( const wchar_t* expression,
                                const wchar_t* function, 
                                const wchar_t* file, 
                                unsigned int line,
                                uintptr_t reserved )
{
    wcout << L"expr:" << expression << endl
          << L"func: " << function << endl
          << L"file:" << file << endl
          << L"line:" << line << endl;

    RaiseException( 0,          // exception code
                    0,          // continuable exception
                    0, NULL );  // no arguments
}

然後透過 _set_invalid_parameter_handler() 來註冊:

wchar_t   dest[ 5 ];
const wchar_t*  src = L"Hello";

_invalid_parameter_handler oldHandler = _set_invalid_parameter_handler( myInvalidParameterHandler );

_CrtSetReportMode( _CRT_ASSERT, 0 );

__try {
    wcscpy_s( dest, _countof( dest ), src );
}
__except ( EXCEPTION_EXECUTE_HANDLER ) {
    cerr << "exception ... " << GetExceptionCode() << endl;
}

_CrtSetReportMode() 在這裡可以幫助 debug mode 下取消 assertion failed 視窗的跳出。如果對於 wcscpy_s() 的 errno_t 的 error code 念念不忘,則可以修改 SEH 的 filter expression 從 EXCEPTION_EXECUTE_HANDLE 到 EXCEPTION_CONTINUE_EXECUTION。

__try {
    errno_t e = wcscpy_s( dest, _countof( dest ), src );

    if ( EINVAL == e ) {
        // ...
    }
    else if ( ERANGE == e ){
        // ...
    }
}
__except ( EXCEPTION_CONTINUE_EXECUTION ) {
}

PS: EINVAL, ERANGE 這類 pre-defined error code 定義於 ERRNO.H 裡頭。

Reference

  1. MSDN on Security Enhancements in the CRT: http://msdn.microsoft.com/en-us/library/8ef0s5kh.aspx
  2. MSDN on Parameter Validation: http://msdn.microsoft.com/en-us/library/ksazx244.aspx
  3. MSDN on _set_invalid_parameter_handler: http://msdn.microsoft.com/en-us/library/a9yf33zb.aspx
  4. MSDN on errno, _doserrno, _sys_errlist, and _sys_nerr: http://msdn.microsoft.com/en-us/library/t3ayayh1.aspx

Windows + Visual Studio + VSCode + CMake 的疑難雜症

Environment Windows 10 Visual Studio 2019 CMake 3.27.7 VSCode VSCode CMake Tools 1. CMAKE_BUILD_TYPE 是空的 參考一下 這篇 的處理。 大致上因為 Visual...