1

I don't understand this C26485 warning I receive.

My code is an exception handler:

catch (CDBException* e)
{
    TCHAR szError[_MAX_PATH];
    e->GetErrorMessage(szError, _MAX_PATH);
    AfxMessageBox(szError);
}

It is saying:

Expression szError: No array to pointer decay (bounds.3).

Both GetErrorMessage and AfxMessageBox are flagging this.

It doesn't make sense to me. Thanks for your help.

GSerg
  • 76,472
  • 17
  • 159
  • 346
Andrew Truckle
  • 17,769
  • 16
  • 66
  • 164
  • That link seems to explain pretty well why this diagnostics exists and how to make it go away? Note that MFC is [very old](https://stackoverflow.com/a/37253321/11683). – GSerg Oct 17 '21 at 18:45
  • 2
    "*An explicit cast to the decayed pointer type prevents the warning*" - thus you can do: `e->GetErrorMessage(static_cast(szError), _MAX_PATH); AfxMessageBox(static_cast(szError));` Alternatively: `TCHAR *pError = static_cast(szError); e->GetErrorMessage(pError, _MAX_PATH); AfxMessageBox(pError);` – Remy Lebeau Oct 17 '21 at 19:02
  • Why not just replace TCHAR with CString? CString has the method PreAllocate(..). – Tom Tom Oct 18 '21 at 23:55
  • @TomTom in 20+ years I have never used Preallocate! I would have to fully understand why to use it. But the function I am using will not accept a CString as a parameter. See Microsoft’s own example: https://learn.microsoft.com/en-us/cpp/mfc/reference/cfileexception-class?view=msvc-160#geterrormessage – Andrew Truckle Oct 19 '21 at 04:08
  • 1
    I'm confused why the call to `GetErrorMessage` would issue this warning. The MFC source has appropriate SAL annotations (at least in VS 2019), communicating to the compiler that both arguments are related(`_Out_writes_z_(nMaxError)`). The array gets decomposed into a pointer/length pair, with the compiler able to see through everything. It **knows** that the pointer/length pair captures the full array information, and that it is correct, too. Maybe this is a bug in the static code analyzer. – IInspectable Oct 19 '21 at 09:09
  • @IInspectable Probably! I don't see why any core SDK code should flag errors! Particularly when the code is used as advertised. – Andrew Truckle Oct 19 '21 at 09:13
  • `GetErrorMessage()` folllowed with `AfxMessageBox()` ? For this we have `ReportError()`. Also in this example you have forgotten to delete the MFC Exception. `e->Delete()`! – Tom Tom Oct 19 '21 at 10:32
  • @AndrewTruckle Again there is need to use TCHAR. e->GetErrorMessage (err.GetBufferSetLength(_MAX_PATH)), _MAX_PATH)); This is what I mean with Preallocate() too. – Tom Tom Oct 19 '21 at 10:38
  • 1
    So, well, this should be categorized as a bug in the static code analyzer, but [Microsoft decided not to](https://developercommunity.visualstudio.com/t/C26485-false-positive/250721). *"It's by design, you suck, we rock!"* I get the message. And now I'm torn in between re-submitting an issue, or just letting go, because I'm not prepared for the level of frustration I would need to endure when dealing with a corporation that has quite obviously abandoned all interest in supporting platform-native developers. Maybe I can challenge myself by turning this into a psychological experiment... – IInspectable Oct 19 '21 at 10:43
  • @TomTom Why don't you offer a answer with working code? Just a suggestion. – Andrew Truckle Oct 19 '21 at 10:50
  • 1
    @tom Using a `CString` silences the code analysis, but not because it is any safer. It just confuses the analysis enough to not see the array-to-pointer decay any more. And that's not even all: It does so by incurring a heap allocation, and the opportunity to throw another exception. The least thing you want in an exception handler is for it to fail, leaving you empty-handed. You can much more easily fool the code analysis by passing `&szError[0]` instead. No heap allocation required, and no exceptions either. – IInspectable Oct 19 '21 at 10:55
  • @IInspectable So `e->GetErrorMessage(static_cast(&szError[0]), _MAX_PATH);` ? – Andrew Truckle Oct 19 '21 at 11:34
  • 1
    That's doing too much. Even if valid, you only need either one of these: `static_cast(szError)` or `&szError[0]`. Though, really, this is a spurious warning (on both function calls, actually), that you do not need to address. If you keep the code as-is, nothing bad will come of it. It's perfectly valid, and doesn't have the slightest chance of failing. If you can hold back, you may just want to temporarily disable the warning for this section of code (accompanied by a `// TODO:` comment), and see whether my issue re-report gains any traction. I'll link here when it's posted. – IInspectable Oct 19 '21 at 11:55
  • 1
    [There](https://developercommunity.visualstudio.com/t/c26485-ignores-sal-annotations/1558068), that one took a bit. – IInspectable Oct 19 '21 at 15:47

2 Answers2

3

This diagnostic is an unfortunate result of the code analysis taking too narrow a view, ignoring hints that are readily available. C26485 warns against array-to-pointer decay, one of C++' most dangerous features. When passing the name of an array to a function that expects a pointer, the compiler silently converts the array into a pointer to its first element, thereby dropping the size information that's part of the array type.

Clients that call into an interface that accepts individual arguments (pointer and size) to describe an array must make sure that the size actually matches that of the array. This has caused countless CVE's, and there's no reason to believe that the situation is getting any better. Array-to-pointer decay is dangerous and having tooling guard against it is great. In theory.

Here, however, things are different. The declaration (and definition) of GetErrorMessage have SAL Annotations that allow the compiler to verify, that pointer and size do match, at compile time. The signature is as follows:

virtual BOOL GetErrorMessage(_Out_writes_z_(nMaxError) LPTSTR lpszError,
                             _In_ UINT nMaxError,
                             _Out_opt_ PUINT pnHelpContext = NULL) const;

The _Out_writes_z_(s) annotation establishes a compile-time verifiable relationship between the pointer lpszError and its corresponding array's size nMaxError. This is helpful information that should be taken advantage of whenever possible.

First, though, let's try to address the immediate issue, following the recommendation from the documentation:

An explicit cast to the decayed pointer type prevents the warning, but it doesn't prevent buggy code.

The most compact way to turn an array into a pointer to its first element is to literally just do that:

catch (CDBException* e)
{
    TCHAR szError[_MAX_PATH];
    e->GetErrorMessage(&szError[0], _MAX_PATH);
    AfxMessageBox(&szError[0]);
}

This fixes the immediate issue (on both function calls, incidentally, even if for different reasons). No more C26485's are issued, and—as an added bonus—passing an incorrect value as the second argument (e.g. _MAX_PATH + 1) does get the desired C6386 diagnostic ("buffer overrun").

This is crucially important, too, as a way of verifying correctness. If you were to use a more indirect way (say, by using a CString, as suggested here), you'd immediately give up on that compile-time verification. Using a CString is both computationally more expensive, and less secure.

As an alternative to the above, you could also temporarily suppress the C26485 diagnostic on both calls, e.g.

catch (CDBException* e)
{
    TCHAR szError[_MAX_PATH];
    // Decay is safe due to the _Out_writes_z_ annotation
    #pragma warning(suppress : 26485)
    e->GetErrorMessage(szError, _MAX_PATH);
    // Decay is safe; the previous call guarantees zero-termination
    #pragma warning(suppress : 26485)
    AfxMessageBox(szError);
}

Which of those implementations you choose is ultimately a matter of personal preference. Either one addresses the issue, with the latter being maybe a bit more preserving as far as code analysis goes.

A word on why the final call to AfxMessageBox is safe: It expects a zero-terminated string, and thus doesn't need an explicit size argument. The _Out_writes_z_(s) annotation on the GetErrorMessage signature makes the promise to always zero-terminate the output string on return. This, too, is verified at compile time, on both sides of the contract: Callers can rely on receiving a zero-terminated string, and the compiler makes sure that the implementation has no return path that violates this post-condition.

IInspectable
  • 46,945
  • 8
  • 85
  • 181
1

As suggested by Andrew, I should post working code here.

Solution without using TCHAR

catch (CDBException* e)
{
   e->ReportError();
   e->Delete();
}

.

catch (CException* e)
{
  CString strError;
  e->GetErrorMessage(strError.GetBufferSetLength(_MAX_PATH), _MAX_PATH);
  strError.ReleaseBuffer();
  AfxMessageBox(strError);
  e->Delete();
}

For the second example, selon IInsepctable this silences only the code analysis.

And to remember, usually you must delete the CException pointer, which you have forgotten here?

Tom Tom
  • 1,127
  • 11
  • 25