21

In my program I use the Running Object Table (ROT) to ensure only one instance of my program is running. Since I "inherit" that code from a developer who unfortunately left the company, I am the poor guy to solve the problems. The code works fine, but we have 3 customers (out of 39,000) who will get an AccessDeniedException. Every customer runs the software in user mode.

Any suggestions what could be wrong?

bool retVal = false;
IMoniker[] arrMoniker = new IMoniker[1];
IBindCtx bindCtx = null;
string displayName;
int hResult;
int mkSys;
Guid clsidRot;
bool guidCompare = false;

IntPtr number = IntPtr.Zero;
moreObjectsListed = false;
objectFromRot = null;

try
{
    // check the objects in the running object table for fitting the specified class id
    while ((retVal == false) && (0 == enumMoniker.Next(1, arrMoniker, number)))
    {
        hResult = CreateBindCtx(0, out bindCtx);
        if (hResult == 0)
        {
            arrMoniker[0].IsSystemMoniker(out mkSys);

            if (mkSys == 4)
            {
                try
                {
                    // the display name is the class id of the object in the table
                    // --> AccessDeniedException raises here <--
                    arrMoniker[0].GetDisplayName(bindCtx, null, out displayName);
                    clsidRot = new Guid(displayName.Substring(1));  
                    guidCompare = clsidRot.Equals(clsid);
                }
                catch(Exception) {}

                // an object with fitting class id was found
                if (guidCompare == true)
                {
                    rot.IsRunning(arrMoniker[0]);
                    rot.GetObject(arrMoniker[0], out objectFromRot);
                    retVal = true;
                }
            }
        }
    }
}
finally
{
    if (arrMoniker[0] != null)
    {
        moreObjectsListed = true;
        Marshal.ReleaseComObject(arrMoniker[0]);
    }
    if (bindCtx != null)
    {
        Marshal.ReleaseComObject(bindCtx);
    }
}

Edit: Here is the requested code for the registration of an object in the ROT:

internal static extern uint RegisterActiveObject([MarshalAs(UnmanagedType.IUnknown)]object pIUnknown, ref Guid refclsid, uint flags, out uint pdwRegister);
internal const uint ActiveObjectStrong = 0;

...

NativeMethods.RegisterActiveObject(this, ref guid, NativeMethods.ActiveObjectStrong, out this.runningObjectTableRegisteredId);

Edit 2:

First of all a big EXCUSE to all investigators, we don't get an AccessDeniedException it is an System.UnauthorizedAccessException (HRESULT: 0x80070005 (E_ACCESSDENIED)).

Second the answers to the questions of "investigator" Ken Brittain: - SharePoint is not in the mix - I'am shure to request the correct object from ROT - Another hint maybe that 1 of the 3 problem (besides 39,000 working correctly) is running the apps on a WTS (Windows Terminal Server)

Edit 3:

Here is a stack-trace of one of those exceptions: (I've translated the stacktrace, because it was on a german machine)

System.UnauthorizedAccessException: Access denied (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
at System.Runtime.InteropServices.ComTypes.IRunningObjectTable.EnumRunning(IEnumMoniker& ppenumMoniker)
at Datev.Framework.DirectStart.RunningObjectTableClientManager..ctor()

The rest of the stack trace is in our code. Markable in this case is that the exception is raised in the constructor of our RunningObjectTableClientManager. Here is the code of that constructor:

private IRunningObjectTable rot;
private IEnumMoniker enumMoniker;

public RunningObjectTableClientManager()
{
    int retVal = GetRunningObjectTable(0, out this.rot);

    if (retVal == 0)
    {
        rot.EnumRunning(out this.enumMoniker);
    }
}
Fischermaen
  • 12,238
  • 2
  • 39
  • 56
  • You don't know where the AccessDeniedException is being thrown? – Pakman Nov 21 '11 at 16:43
  • @Pakman: Hardly to find, because the problem only comes up on customers pc running the program in release build. But with some traces I could find out that accessing the `arrMoniker[0]` raises the exception. One of it (`arrMoniker[0].GetDisplayName(...` I tried to put in a try-catch. That prevented the exception from being raised, but then the functionality wasn't given (Our program is instanciated again and again) – Fischermaen Nov 21 '11 at 16:47
  • It would definitely make things much easier if you could obtain the exception's message when it's thrown. Does your program have logging that can be enabled in Release? Maybe you could have the failing clients try to run the tool: IRotView? – Pakman Nov 21 '11 at 17:08
  • @Pakman I marked the line where the exception raised. – Fischermaen Nov 21 '11 at 18:35
  • What mode can you run OLE software in other than user mode? Certainly you can't run that code in kernel mode. – Gabe Nov 21 '11 at 18:40
  • @TonyLee: I can supply that code in about 12 hours, I'm at home now. – Fischermaen Nov 21 '11 at 18:55
  • This is a speculative guess, but given that only 3 out of 39,000 have observed this issue, it seems more likely that the exception is caused by a 3rd party component that is registered in the ROT, perhaps as another identity other than the logged-in user. Your code looks fine to me. Can you get a list of running processes and installed software from those customers? – meklarian Nov 21 '11 at 18:59
  • @meklarian: I will do my best to get such a list. Do you think that someone else register under a key of us? We us a GUID to register, that should be unique. What makes me wonder is, that I get the moniker, but when I access `.GetDisplayName()` I get the mentioned exception. hmpf... – Fischermaen Nov 21 '11 at 20:04
  • 1
    The exception occuring at `.GetDisplayName()` is exactly what makes me suspicious of other foreign objects in the ROT. The ROT is a shared resource, so any application that wishes to use it can place objects there. You can observe this in action by launching MS Word or MS Excel and noting the presence of their open Documents in the ROT. Another angle of attack is to make a test program to dump the GUIDs in the ROT while running as an account with administrator rights. This could provide extra clues. A GUID collision would fail elsewhere, not in the sample you've provided. – meklarian Nov 21 '11 at 20:34
  • Also note that Monikers are based upon named parts that are meaningful only to the provider of a Moniker. It's pretty convenient to register with a GUID because they're generally unique, but doing this doesn't prevent someone else from trying to register a name that has a part that contains the same GUID. – meklarian Nov 21 '11 at 20:37
  • As far as I remember the ROT is not truly global. Could it be that users are logging via remote desktop onto one machine and try to launch your app under different user/desktop sessions? – Alois Kraus Nov 21 '11 at 22:32
  • I've added the code for registration of an object in the ROT. – Fischermaen Nov 22 '11 at 06:11
  • @AloisKraus No user is logging in via remote desktop. – Fischermaen Nov 22 '11 at 06:11
  • can you provide the message field and the stack trace of the exception? – DarkSquirrel42 Nov 24 '11 at 14:25
  • @DarkSquirrel42: I've added it in part "Edit 3" at the end of my question – Fischermaen Nov 24 '11 at 14:51
  • 1
    Is this post related? http://social.msdn.microsoft.com/Forums/da-DK/clr/thread/fbda9cdd-77b0-4d1d-bfdd-cb23c167f536 – user7116 Nov 24 '11 at 15:45
  • @sixlettervariables: Thank your for the link - I've seen this post before. The funny thing is, that the trouble is only happening on 3 of 39,000 installations, so I think it can't be a mistake in general. – Fischermaen Nov 24 '11 at 21:57
  • Could it be that due to session 0 isolation you are not able to access the ROT of other users? You said no user is logging in via remote desktop but you are using a terminal server. Am I right that several users log in to the same machine and you wan to share a cross user pointer? – Alois Kraus Nov 25 '11 at 21:25
  • @AloisKraus: No, we are not sharing across the user. The trouble with the terminal server is only on 1 of the 4 (there was one new problem coming in today) machines making trouble. The whole thing makes me nervous, because I can't find out any system in that error. – Fischermaen Nov 25 '11 at 21:34
  • I do not know how the running object table is implemented but a guess would be that one process having more rights is started first and the others can access it via the rot without issues. It could have something to do with the creation of objects in the Global Kernel namespace. You could give your processes the Create Global objects privilege. – Alois Kraus Nov 25 '11 at 22:02
  • A little question - *why* is the check made? Are you creating a single-instance application that just needs the functionality of the "Make Single Instance Application" checkbox? (Well, I take it this is more complicated than that, but still...) – Ry- Dec 01 '11 at 01:39
  • It appears that are registry settings around the security of the table (and perhaps the objects in the table). See http://graphcomp.com/info/specs/com/comdocs/ch08secu.htm – shf301 Dec 01 '11 at 02:37
  • @minitech: our company offers a bunch of more than 120 programs to our customers. All these programs can call eachother. Some of the programs are quite old (written in C++) and most of them are written in C#, but all of them can be called via running object table, if they are already running. – Fischermaen Dec 01 '11 at 06:27

3 Answers3

5

In my experience the probability of a GUID collision, while possible appears unlikely, so it was not investigated. The first track I took was looking what could cause the AccessDeniedException. Working backward from there you can see that GetDisplayName does not explicitly throw this exception (or return anything similar).

So what does? Your code appears to be in C#. Unless I am mistaken using COM from C# will go through a primary interop. There are only two (2) interops that expose an IMoniker interface that I could find:

  • System.Runtime.InteropServices.ComTypes contains IMoniker
  • Microsoft.VisualStudio.OLE.Interop contains one as well IMoniker

You are talking about an application so my gut tells me you are using the runtime version. Looking at the calls I could not find a call returning any form of an Access Denied HRESULT or simething similar. The VisualStudio interop does mention the following about access and trust: Using Libraries from Partially Trusted Code. This sounded like a path to follow and would apply if your are using the Visual Studio interops.

If you are using the runtime services namespace which is contained in the mscorlib.dll assembly (which according to this page .NET Framework Assemblies Callable by Partially Trusted Code is marked as callable partially trusted code) the explanation does not appear to apply.

So now what? I did a search for AccessDeniedException and found no supported implementation other than an Microsoft.Office.Server.ApplicationRegistry.Infrastructure.AccessDeniedException class that is marked as obsolete in MSDN. The class is filed under the SharePoint 2010 class library.

So here are my questions: Which interop are you using? Is SharePoint in the mix at all? I said previously GUID collision was not suspected but now I am questioning that assumption. Are you requesting the proper object from the ROT? Is this object running under another process (meaning not yours)?

Ken Brittain
  • 2,255
  • 17
  • 21
  • Lots of question! I will do my best to find an answer to all you metioned. As soon I have found out more, I'll post it here. Thank you very much for you help! – Fischermaen Nov 22 '11 at 13:37
  • Dear Ken, I've answered some of your question in the "Edit 2" section at the end of my question. – Fischermaen Nov 24 '11 at 13:42
  • Unfortunately nothing helped. +1 for your effort. Thanks. – Fischermaen Nov 30 '11 at 10:24
  • Sorry I have been out of it (sick as of late). I copied your code into VS2010 and debugged. Found out the the IRunningObjectTable member is really of type System.__ComObject (a wrapper for COM interfaces from the CLR). This makes sense as you probably PInvoked the GetRunningObjectTable call...correct? So SO search for "__ComObject E_ACCESSDENIED" (the original HRESULT) brings up to http://stackoverflow.com/questions/6503136/queryinterface-fails-with-e-accessdenied. I would suggest looking into why it fails on the client (apparently due to permissions). – Ken Brittain Dec 02 '11 at 16:31
1

From this site it appears it could be due to a registry setting or due to the security settings on an object registered in the table:

           Check "HKLM\Software\Network OLE\Enabled". Fail the    
           request if zero.                                       

           Check "HKCU\Software\Network OLE\Enabled". Fail the        
           request if zero.                                           
           Before performing any operation against a ROT entry        
           (i.e., IRunningObjectTable::Revoke,                        
           IRunningObjectTable::IsRunning,                            
           IRunningObjectTable::GetObject,                            
           IRunningObjectTable::NoteTimeChange,                       
           IRunningObjectTable::GetTimeOfLastChange, or when          
           including an entry in an IEnumMoniker::Next of an          
           IEnumMoniker returned from                                 
           IRunningObjectTable::EnumRunning), check the call against  
           the SECURITY_DESCRIPTOR available from                     
           IRunningObjectTable::Register. This will be either the     
           value returned by the object's                             
           IActivationSecurity::GetSecurityDescriptor at the time of  
           IRunningObjectTable::Register or will have been taken      
           from "HKCU\Software\Network OLE\DefaultROTSecurity" or     
           "HKLM\Software\Network OLE\DefaultROTSecurity" at the      
           time of IRunningObjectTable::Register if the object did    
           not support IActivationSecurity.
shf301
  • 31,086
  • 2
  • 52
  • 86
1

Perhaps this isn't the answer you are looking for, but having inherited this code, have you stopped to question if this was even the right technique for your use case? This is the first time I've seen a C# app use Com Interop for something like preventing multiple app instances. I've never had good experiences with Com and found similar unexplained or undocumented exceptions.

Why not take a look at an alternative technique for preventing multiple application instances? I have used a Mutex in past solutions of mine and never had an issue. While I don't have my past code handy, this issue has been covered several times before on stackoverflow with some pretty good answers that have been peer reviewed and community edited.

For example 'What is a good pattern for using a Global Mutex in C#?' has a good community edited answer that seems to take into account all sorts of odd ball race conditions and thread/process terminations as well as the potential security issues.

So my recommendations would be to step away from Com Interop and grab a Mutex implementation instead.

Community
  • 1
  • 1
BenSwayne
  • 16,810
  • 3
  • 58
  • 75
  • 1
    You absolutel right, and I've asked myself if it's the right technique, *but* our company has got 1,000 developers making more than 120 applications. Some of them are quite old, all of them have this technique in it and are able to call eachother. We are on the way to change the technique of connecting our programs, but we can't do it "in a day". Right now I have a problem with 4 of 39,000 customers I have to solve. So basically it's working... – Fischermaen Dec 01 '11 at 07:33