11

Related to e.g.: How to poison an identifier in VC++?
What does it mean to "poison a function" in C++?

Is there a way to "properly" poison some functions whose declarations (or implementations) are not under my control?

Specifically, I'm trying to prevent accidential use of certain Windows API functions which may result in surprise results, such as e.g. CreateFileA (where use of the T macro hides the fact that you mix ANSI and Unicode) or SetWindowLong (which will cause your program to fail silently, without error, and without a chance of knowing what went wrong on a 64-bit system).

My definition of poisoning "properly" is that attempting to call the function breaks the build with a readable error (bonus points if the error occurs at the correct location in the source code).
Preferrably, this should be portable, and at the very least it must work with both GCC and clang.

What I've tried / looked at so far:

preprocessor

The simplest, immediately obvious solution would be to exploit the preprocessor:

#define CreateFileA __poison_ANSI_CreateFileA

This works very well for that one particular function as well as a couple of others, working exactly as intended, with a precise error that hints to the problem and points to the correct location in the source:

error: no matching function for call to '__poison_ANSI_CreateFileA'
note: expanded from macro 'CreateFile'
note: expanded from ... (precise location in source)

For different functions, one needs to have a unique name for each to avoid conflicting definition errors, which is tedious but easily done. So far so good, except it does not work for functions where the MinGW-w64 headers themselves tamper names using variadic macros, such as for example CreateWindow or SetWindowLong. No matter what, these compile just fine, poisoned or not.

pragma

#pragma GCC poison, which clang happens to understand as well (and also features its own version), looks and sounds like it should do exactly what I want, and in the most idiomatic way possible. Alas, it does not work. Or rather, it works too well.
Functions poisoned with the pragma directive already trigger an error when the poisoned name appears in a declaration, which means... every time, in every build that includes the header at all. That's not very helpful for writing a program!

attributes

These have the immense disadvantage that you must copy exactly the type definition lest you get "ambiguous function call" errors on legitimate calls on the one hand side. On the other hand side, they don't work. clang complains about __attribute__((__error__)) but ignores gnu::__error__ or gnu::error as well as gnu::deprecated. Further, the standard [[deprecated]] is all nice and well, but seems to do exactly nothing (compiles fine, not even a warning?!).

Is there anything I can do?

Community
  • 1
  • 1
Damon
  • 67,688
  • 20
  • 135
  • 185
  • CreateWindow is itself a macro for CreateWindowEx(A/W) did you try poisoning those definitions ? – ACB Jan 25 '17 at 17:58
  • @ACB: How could I? I could only `#define` the macro-macro with something else (optionally `#undef` first), but that will make whether or not it works or silently fails, or gives a "macro redefined" warning dependant on whether I include `"poison.h"` or `` first. Since you cannot always know which headers are included in which order, that's a path to desaster.. :( – Damon Jan 27 '17 at 11:45
  • As a work around, you could define the offending function into a macro that generates an error. #define CreateFileA() #error unsafe function CreatFile1 – Gregg May 18 '18 at 20:06
  • @Gregg How would that be in any way different from the preprocessor hack that I used? The problem with these is that they _don't_ work when a variadic macro is being used in the system headers. Which, unluckily is the case with about half of the cases that I'd like to poison... – Damon May 19 '18 at 19:01
  • "Poisoning CreateFileA" suggests to me that you aren't really understanding the problem in the first place. It's the `T` macro and the `CreateFile` pseudo-overload that you want to poison. Those are Windows-95 hacks. – MSalters Jul 29 '18 at 11:01

2 Answers2

1

Have found a way to enforce ambiguity by adding another declaration of the same function in a namespace. No pragmas or attrbiutes are used, should be portable to any C++11 compiler:

#include <stdio.h>

#define POISONED_BY_NAME(name) \
    namespace Poisoned \
    { \
        using function_type = decltype(::name); \
        extern function_type* name; \
    } \
    using namespace Poisoned;


POISONED_BY_NAME(puts)

int main()
{
    puts("Hello"); 
}

Visual Studio message:

error C2872: 'puts': ambiguous symbol
1>c:\program files\windows kits\10\include\10.0.17763.0\ucrt\stdio.h(353): note: could be 'int puts(const char *)'
1>c:\test.cpp(43): note: or       'Poisoned::function_type (__cdecl *__cdecl Poisoned::puts)'

gcc message:

main.cpp: In function 'int main()':
main.cpp:16:5: error: reference to 'puts' is ambiguous
     puts("Hello");
     ^~~~
main.cpp:12:22: note: candidates are: 'int (* Poisoned::puts)(const char*)'
     POISONED_BY_NAME(puts)
                      ^~~~
main.cpp:7:35: note: in definition of macro 'POISONED_BY_NAME'
             extern function_type* name; \
                                   ^~~~
In file included from main.cpp:1:
/usr/include/stdio.h:695:12: note:                 'int puts(const char*)'
 extern int puts (const char *__s);
            ^~~~
Alex Guteniev
  • 12,039
  • 2
  • 34
  • 79
  • 1
    Thank you for the attempt, this indeed works for API functions which are declared as functions (such as `puts` in your example). Unluckily, it _doesn't work_ for functions that are in reality macros or macros-of-macros, such as `CreateWindow` which expands to `CreateWindowA`, which expands to `CreateWindowExA` (at least under MingW-w64, not sure about MS platform headers). In that case, there is no way of building any program with the above (always breaks build). – Damon Mar 03 '19 at 10:56
  • It works for me if I use `POISONED_BY_NAME(CreateWindowExA)` or `POISONED_BY_NAME(CreateWindowExW)` (depending on UNICODE macro). Or was your intention to poison `CreateWindow` but not `CreateWindowExA` ? – Alex Guteniev Mar 03 '19 at 11:02
  • The intent was to prevent me (or whoever) from accidentially calling e.g. `CreateWindowA` (explicitly or by forgetting to define the unicode macro and calling `CreateWindow`). Or, accidentially using `SetWindowLong` instead of `SetWindowLongPtr`, which will cause untraceable future desaster. So, if poisoning `CreateWindowExA` worked, that would be the solution. Unluckily, for me, this breaks the poison code like this: `error: conflicting declaration 'using function_type = ... using function_type = decltype(::name); note: in expansion of macro 'POISONED_BY_NAME'` (edited to fit comment) – Damon Mar 03 '19 at 11:11
  • I see. In `x86` `SetWindowLongPtr` is defined to be implemented via `SetWidnowLong`. So ultimately your goal cannot be achieved by poisoning functions at all, if solution existed, it would be poisoning macro names. – Alex Guteniev Mar 03 '19 at 11:19
  • Well in 32 bits, it's fine to implement `SetWindowLongPtr` as `SetWindowLong`, so if the API doest that, that's no problem. But in 64 bit builds, it sure is a proper function. You _might_ however accidentially use the wrong one. Which will, unluckily, "work fine". Only just... it truncates half the pointer's bits, so depending on memory layout, you get a crash at an unpredictable, unrelated time later, and have no way of knowing why the hell the program is crashing (and sometimes works). That's the kind of debug nightmare you want to avoid. – Damon Mar 03 '19 at 11:26
0

I would keep in the pre-processor hack as the first line of defence, as it appears to capture some cases quite well, giving you the exact location within the source code.

I would also put in a second layer of defence, by ensuring you provide an object file containing your own variants of the functions you want poisoned, something like:

HWND WINAPI CreateWindowEx(DWORD dwExStyle, LPCTSTR lpClassName,
    LPCTSTR lpWindowName, DWORD dwStyle, int x, int y, int nWidth,
    int nHeight, HWND hWndParent, HMENU hMenu, HINSTANCE hInstance,
    LPVOID lpParam)
{
    std::cerr << "Attempting to use poisoned function CreateWindowEx\n";
    exit(1);
}

If you link this as an object file (not library, since a library will only link on unresolved references), there will be three possible cases (once it's gotten past the header poisoning):

  • Both your object file and the real code will be linked and you will see a duplicate definition at link time.
  • Only your object file is linked (not the real one) so, when you call it, you'll get the error message and termination. This should be found pretty early at test time. Not during compilation/linking as you requested, but hopefully well before you ship.
  • Only your object file is linked and you don't call it. In that case, you've wasted a little bit of code space but your executable will run fine.

In terms of doing this, a single poisoned.h and poisoned.c should do the trick, you just have to ensure that all functions you want poisoned are added to both.

Now I haven't tested that this method would work with MinGW but it's worth giving it a try since, at the lowest level, it will need to call the real Windows function at some point.

The only way I could envisage your issue with MinGW happening is that it's calling some already-compiled MinGW objects that eventually call the Windows function. If that's what happening then the linker capture should catch this as well.

paxdiablo
  • 854,327
  • 234
  • 1,573
  • 1,953