Dan Byström’s Bwain

Blog without an interesting name

Shall we play /unsafe?

Posted by Dan Byström on September 10, 2006

First of all, I must admit that I’m not a big fan of computer security. I don’t even run any anti virus software on any of my computers. I tried it briefly some ten years ago and my computer behaved odd all the time so I turned it off. I don’t think things are much better now. Just a couple of moths ago a friend of mine spent a day cursing and kicking his computer because an important program he had to run for his project (some sort of FTP program I think it was) refused to work. I asked him a couple of days later if he had solved the problem. You guessed it: as soon as he turned off his anti virus software he was back on track!

Of course, computer security is a terribly important field and much larger and more complicated than most of us can imagine. Nevertheless – I’ll whine over it a little anyway!

(I often get the question why I don’t get viruses when I don’t use any virus protection. The answer is very simple: when I receive a virus I don’t run it!)

Now I shall tell you a little story from a programmer’s day of life. I’ve written a piece of software that’s run by some 70 people traveling out in the fields, who several times a week need to send in large amount of data to the headquarter. Typically this data is around 3 GB or so (but can be much larger), and we can’t rely on them having access to the internet. So the solution is of course to burn the data on DVD records and send them using snail mail.

For this I have used XPBurn, which I got to work after fixing a few bugs in the source code (which I reported back to the author who released a new version but didn’t give me any credit. C’est la vie). Now a reoccurring problem for me with .NET in general and XPBurn in particular is that they both tend to lock files and refuse to let go of them.

For example, after burning files with XPBurn I have to restart my app in order to burn them again, because they’re locked. There probably is a simple solution to this, but I haven’t been able to find it. Or maybe there isn’t.

.NET is also good at locking files, for example:
  _bmp = (Bitmap)Bitmap.FromFile( "c:\\house.jpg" );
locks the file until you dispose _bmp. The only reason I’ve been able to come up with is that .NET needs to keep multi page TIF files open, and that .NET therefore treats all images the same way. If you have a better explanation, please tell me!

In order to open an image without leaving a lock on the file you need to:
  using ( Bitmap bmp = (Bitmap)Bitmap.FromFile( "c:\\house.jpg" ) )
    _bmp = new Bitmap( bmp );

So, when my program burned DVD records, there where sometimes peculiar errors claiming that some files was locked, although I was 100% positive that I closed them correctly. In order to get around this problem I decided that I needed to break up my burning over – don’t laugh at me now – not two, but actually three different processes (.EXE-files)! If you laugh anyway, please read on!

I split my app into three pieces:

  1. the main application
  2. vdStandaloneBurn: a “mediator” with a UI for the burning process
  3. vdBurnProc: the burning software itself (including XPBurn)

Here’s how it goes: the main app writes an instruction file to disk specifying all files it wants to burn (and if any of them must exist on each spanned disk and the disk labels etc). Then the main app executes the Mediator and then terminates itself! The Mediator reads the instruction file and takes care of any disk spanning necessary (jobs being larger than one DVD). The user may specify how large a “full disk” is, because when using XPBurn, the job will often fail if we try to fill the disk completely. Therefore we have lowered the default “full size” to 4000Mb, which has worked fine. This limit can also be brought down below 700Mb in order to use CD records.

For each record, the Mediator then executes the actual burner, vdBurnProc, and gets notified on progress as well as completion results, offering to re-burn a failed record etc. Now where’s the point in all this? Well, the point is that when the main app terminates itself there is no chance that any files remain locked! And since vdBurnProc terminates itself after each burnt record, it can’t lock any files either. And the Mediator only touches the instruction file and nothing else! You see?

When the final record is burnt, the Mediator restarts the main app, which’s work can then continue. Although this may sound unnecessarily complicated, in practice it works like a dream!

Now I guess you wonder where all this leads up to? Can’t say that I blame you, I think I lost myself there too for awhile, but now I will get back on track!

I wanted to keep the UI for the whole burning process in the Mediator and therefore I needed a mechanism to report burning progress from vdBurnProc to the Mediator in some way. What can be easier than using plain old Windows Messages for this? Sending numeric data this way is a breeze, but then I also wanted to be able to send back a result string! Passing a string over process boundaries is not all that simple, unless we cheat! OK, let’s cheat. We use WM_SETTEXT, which for backward compatibility with 16-bit Windows actually moves the string across the process boundary for us! We will get a pointer to the string that was sent from the other process without any fuzz (or so I thought!!!). Also note that when we’re processing the WM_SETTEXT message we’re blocking the sender until we return, so let’s get out of there quickly! The code in the Mediator to listen to what vdBurnProc has to say about its place in life goes like this:

interface IBurnCallback
{
  void AddProgress(int nCompletedSteps, int nTotalSteps);
  void PreparingBurn(int nEstimatedSeconds);
  void BlockProgress(int nCompletedSteps, int nTotalSteps);
  void ClosingDisc(int nEstimatedSeconds);
  uint BurnerHwnd { get; set; }
  void BurnComplete(string strMessage);
  IntPtr Handle { get; }
}

class InteropWindow : NativeWindow
{
  private IBurnCallback cb;
  private string ResultText = null;

  public InteropWindow(IBurnCallback cb)
  {
    this.cb = cb;
    CreateParams cp = new CreateParams();
    cp.Parent = cb.Handle;
    cp.Caption = "vdStandaloneBurn Interop Window";
    this.CreateHandle(cp);
  }

  protected override void WndProc(ref Message m)
  {
    switch ( m.Msg )
    {
      case WM_USER:
        cb.AddProgress( (int)m.WParam, (int)m.LParam );
        return;
      case WM_USER+1:
        cb.PreparingBurn( (int)m.WParam );
        return;
      case WM_USER+2:
        cb.BlockProgress( (int)m.WParam, (int)m.LParam );
        return;
      case WM_USER+3:
        cb.ClosingDisc( (int)m.WParam );
        return;
      case WM_USER+4:
        cb.BurnerHwnd = (uint)m.WParam;
        return;
      case WM_USER+5:
        cb.BurnComplete( ResultText );
        ResultText = null;
        return;
      case WM_SETTEXT:

        unsafe
        {
          ResultText = new string( (char*)m.LParam.ToPointer() );
        }
        // inform the main thread that the result has come
        PostMessage( (uint)this.Handle, WM_USER+5, 0, 0 );
        // and don't block this thread anymore
        return;
      }
      base.WndProc(ref m);
    }
}

And HERE is finally what this blog post is all about! The unsafe keyword! In order to compile this code I need to compile using the /unsafe switch. I did that and I saw it was good. My DVD burning problems were finally gone in one stroke. As I said, this really works like a dream.

End of part one of the story. A year passes. Now I realize that I want to use the same solution in another project. In this case however, I really can be sure that the main app won’t lock any of the files, so I skip the Mediator. I take my vdBurnProc.exe and XPBurn.dll and throw them into the same directory as the new main application and throw in the code above into the app. Then I get a compiler error saying that I must compile using the /unsafe switch. Obediently I comply. I test the app and it works nicely!

Now this app is actually run by different users in two different ways. Some run it through Remote Desktop (Terminal Server). Those users will of course not use the burning function since it will burn records on the Terminal Server machine. When we upgrade the software it is very simple to give those users a new version. We just copy the new version into a new directory and change the desktop icon. The next time the users start the app, they get the new version.

Other users, however, run the application locally, having the binaries on their local hard drives. In order to upgrade the app for them, the following scheme is used:

  1. Each time they start the app, a special place on the network is checked (this path is stored in a table in an SQL Server – excuse me for cursing ;-).
  2. The app checks if a newer version exist on the network by comparing the version numbers like this:
    Version verThis = Assembly.GetExecutingAssembly().GetName().Version;
    Version verThat = Assembly.LoadFile(strOtherVer).GetName().Version;
    int[] anThis = new int[] { verThis.Major, verThis.Minor, verThis.Build, verThis.Revision, 1 };
    int[] anThat = new int[] { verThat.Major, verThat.Minor, verThat.Build, verThat.Revision, 0 };
    for ( int i=0 ; i<anThis.Length ; i++ )
      if ( anThis[i]>anThat[i] )
        return false;
      else if ( anThis[i]<anThat[i] )
        break;
    if ( Global.askMsgBox( null, false, "There exists a new version of this application. Would you like to install it?" ) == DialogResult.Yes )
    {
    ...
  3. If a newer version is found and accepted by the user, it is copied to the local hard disk and restarted.

This is also a solution that has worked like a charm in real life. Until this Tuesday morning when the sysop woke me up and cried HELP.

What went wrong?

Because of the /unsafe switch, the “local” users got a run time error on this line:
  Version verThat = Assembly.LoadFile(strOtherVer).GetName().Version;

They weren’t trusted to load an unsafe assembly from a network drive. $#%&#*¤#!!!!

OK, how do we fix that? A little googling (oh, sorry, Google says this takes their trademark towards “genericide”, whatever that may mean, “A little searching using the Google search engine”, I mean) tells me that the app needs a strong name! Fine, I give it a strong name. Now the compiler tells me I must give ALL my assemblies strong names! Including the old version of NHibernate we’re using. Do I want to go further down this road? Not if I can help it. It’ll probably just get worse. What other options do I have? What is the basic problem here?

I had to use the /unsafe switch in order to execute ONE line of code (out of 200,000) which wanted to use a simple pointer!!! Is it possible to rewrite this single line in some way? All I want to do is to convert a character vector which I’m holding a pointer to into a regular .NET string. Now the Framework says that this single line of code is so dangerous that users aren’t allowed to use my app anymore! *sigh*

Every seasoned VB6 programmer out there immediately knows the answer to this one. In lack of pointers we simulate them using the API function RtlMoveMemory!!! So, visiting www.pinvoke.net we get the signature:

  [DllImport("Kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
  public static extern void MoveMemory(byte[] x, uint src, int size);

Now we can easily rewrite this:

  unsafe
  {
    ResultText = new string( (char*)m.LParam.ToPointer() );
  }

Into:

  ResultText = string.Empty;
  byte[] buf = new byte[500];
  MoveMemory( buf, (uint)m.LParam, buf.Length );
  for ( int i=0; buf[i]!=0 && i<buf.Length; i+=2 )
    ResultText += (char)buf[i];

And now we’re SAFE again! (Oh, should I use a StringBuilder and should I make sure that I correctly handle all Unicode characters? Feel free to fill in that gap in your mind while reading!)

So, what good is this /unsafe business when we can overcome it so easily!?!!? As long as a .NET application is allowed to “pinvoke” to the Windows API it can never be trusted!!! Its code may become a little uglier but it can still accomplish exactly the same thing as “unsafe” pointer arithmetic can do!

Do I leave any recommendations or suggestions regarding this? Or even any conclusion? No, I just wanted to tell my little real life story. Hope you enjoyed it.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: