Dangerous PInvokes - string modification

Objects in CLR are usually managed by the runtime in GC heap; user code does not have direct access to the objects. CLR's reliability and type safety heavily rely on this fact. But CLR also support InterOp features like COM InterOp, IJW and PInvoke to allow unmanaged code to touch managed objects directly. It's very dangerous to use those InterOp techniques without caution. The potential problems include:

 

  1. Security. Unmanaged code could bypass the runtime to access any controlled resources and change any state of a managed program. That's why calling unmanaged code requires very high security permission.
  2. Reliability. Unmanaged code could access managed objects (thus GC heap) directly. So it could corrupt GC heap easily, which is fatal error for CLR. Ideally, in a pure managed world, all bugs in user code should result in an exception or error code; but with mistakes in an InterOp operation, you could find the program just crashes with no clues. Because the corruption happens deeply in CLR, the problem would be very hard to debug for users.

 

InterOp is quite a complicated topic, so it's better for programmers to have deep understanding before writing such code. I would highly recommend Adam Nathan's book “.NET and COM – The Complete Interoperability Guide”, which is the bible of CLR InterOp.

 

Among the three basic InterOp techs, PInvoke seems to be much simpler than the other two. Most time it requires only declaring and calling, like using any managed functions. But I want to remind people some catch-chas of PInvokes. Because:

 

  1. PInvoke calls into pure unmanaged DLL, which has almost no self-description data for CLR. So there's not much validation CLR could perform at compile and run time. (Performance is another reason for little validation at run time)
  2. It's so easy to use, sometimes people forget the danger built-in its unmanaged nature. :)

 

The first topic I want to cover is string's immutability in PInvoke. In one of his wonder blogs, Chris Brumme talked about why immutability is important for strings, especially interned strings. He gave an example about how a string's contents could be changed in place with a PInvoke call:

 

using System;

using System.Runtime.InteropServices;

public class Class1

{

    static void Main(string[] args)

    {

        String computerName = "strings are always immutable";

        String otherString = "strings are always immutable";

        int len = computerName.Length;

        GetComputerName(computerName, ref len);

        Console.WriteLine(otherString);

    }

    [DllImport("kernel32", CharSet=CharSet.Unicode)]

    static extern bool GetComputerName(

        [MarshalAs (UnmanagedType.LPWStr)] string name,

        ref int len);

}

 

This example demos that when you change contents of one interned string, another string might be changed unintentionally. I want to add one more point: string's hash code is calculated using its contents, so changing a string's contents would change its hash code, which violates the invariant that an object's hash code should not change over its lifetime. It's very hard to know if the string you are modifying is used as a hash key anywhere in CLR or user's code. As a matter of fact, in V1.0 and V1.1 CLR stores all interned strings in a hash table across AppDomains. So if an interned string's contents are changed, it could mess up the underlying hash table, and cause CLR to crash mysteriously during later AppDomain loading/unloading.

 

As mentioned in Chris's blog and Adam's book, the solution is to declare a PInvoke to use StringBuilder for any string parameters it might modify. A common pitfall here is that people might believe if an unmanaged function requires a LPTSTR as a parameter, the interface should be declared to use StringBuilder in managed code; if it's a LPCTSTR (or any other constant character pointer), it's safe to be declared as a String. This is not 100% correct because unmanaged code could always cast a constant pointer to be a mutable one. For example, Windows GDI's DrawText API is declared as:

 

int DrawText(

  HDC hDC, // handle to DC

  LPCTSTR lpString, // text to draw

  int nCount, // text length

  LPRECT lpRect, // formatting dimensions

  UINT uFormat // text-drawing options

);

 

The lpString here looks to be constant, but description of MSDN says:

 

lpString

[in] Pointer to the string that specifies the text to be drawn. If the nCount parameter is –1, the string must be null-terminated.

If uFormat includes DT_MODIFYSTRING, the function could add up to four additional characters to this string. The buffer containing the string should be large enough to accommodate these extra characters.

In Appendices E of his book, Adam suggests managed declaration of this function could be:

[DllImport (“User32”, CharSet=Charset.Auto)]

static extern in DrawText (IntPtr hDC, string lpString, int nCount, ref RECT lpRect, uint uFormat);

 

This is right for most of time, but such a usage could cause problem:

 

string text = "Hello world!";

DrawText (hDC, text, text.Length, ref rect, DT_END_ELLIPSIS | DT_MODIFYSTRING);

 

If the function tries to append some additional characters to the string, it will write out of range of this string object and overwrite other objects in the managed heap, which would corrupt the heap and might crash CLR eventually.

 

The point I want to make is that PInvoke might look simple, but it's still dangerous (because all unmanaged code is dangerous for CLR :)). Now CLR can't check correctness of a PInovke signature automatically. Instead, we have some run time debugging tools like CDPs for some particular checking around PInvoke. Adam's blogs has very good coverage on them. In the long run we may be able to do some analysis using static tools but there's no specific plan at this time. So to avoid any PInvoke problem, users need to understand it very well and read the documents of the unmanaged function carefully. If you are experiencing any unexplainable crash in a .NET program, it might be worthy to check all the PInvokes in the code.

 

 

This posting is provided "AS IS" with no warranties, and confers no rights.