Scott Hanselman

August Madness - A tale of Classic ASP, Two CLRs, IIS, and COM Interop

August 22, '06 Comments [16] Posted in ASP.NET
Sponsored By

UPDATE: Here is how we solved this problem.

So, this is bizarre. The idea is this:

We want to share FormsAuth cookies between two ASP.NET sites on the same box. No biggie, right? However, one of the ASP.NET sites also has some Classic ASP pages in it. Still, no worries, right?

  • The Classic ASP pages happen to use .NET objects that expose themselves to Classic ASP via COM Interop…
  • If ASP.NET pages are hit ONCE in both sites (pools/appdomains) to 'bootstrap' the CLR, all is well and FormsAuthTickets will be sharable and jointly decryptable.
    • .NET 1.1 and .NET 2.0 are installed on the systems
    • Both ASP.NET applications/vdirs are configured to use .NET 1.1 in IIS

PROBLEM: If the site that creates the FormsAuthenticationTickets instead has the CLR 'bootstrapped' into the process/pool/domain by hitting a Classic ASP page that calls the COM-interop DLL, it causes later FormsAuthentication crypto to produce tickets that can't be decrypted by the other application.

QUESTION: What's going on? Both sites have web.config files with synchronized machinekeys (the machinekey is used by FormsAuth).

PARTIAL ANSWER: Turns out that if the Classic ASP page (that uses the .NET object that is used via COM) is hit first then .NET 2.0 gets loaded up inside that IIS Managed Application even if the ASP.NET application is set to use .NET 1.1!

Am I missing something? Does this mean that I can't have two classic ASP apps that use .NET objects via Interop, one using 1.1 and one using 2.0 on the same computer? Seems like it.

The issue is that ASP.NET doesn't start up the CLR because it's not hit first. The Classic ASP page is hit first, and apparently can't control the CLR version it gets! By the time someone hits an ASP.NET page, the CLR is already loaded up with a different version.

HERE'S THE KICKER: I don't yet understand why (on Win2k3, in our tests) the problem only happens when the anonymous identity for the web site is a domain account.  If the anonymous identity was left as the default IUSR_xxxx, the CLR loaded is still 1.1 when a CCW is created. (!)

OUR WORKAROUND: We just let .NET 2.0 load up anyway and run our 1.1 application. We have to synchronize the machinekeys in BOTH CLR versions' machine.config files though.

Kudos to Peter Wong for figuring this stuff out. Now the question is - is there a good workaround or better explanation? My guess is we'll hear "functions as designed" but it seems to me that even though you can't indicate in the COM Registration stuff in the Registry what CLR Version to use (it's listed in the Registry, but the values are ignored) there should be some way.

Repro attached. Unzip this file into a c:\inetpub\wwwroot\wong test and make an IIS Application (isolated, or in its own AppPool in IIS). Get Process Explorer from SysInternals and hit x.asp after a fresh reset.

File Attachment: NET11_NET20_ASP.zip (894 bytes)

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web
Tuesday, August 22, 2006 10:31:31 PM UTC
"However, one of the ASP.NET sites also has some Classic ASP pages in it. Still, no worries, right?"

A good question to ask here would be, "Is the amount of time and effort spent debugging this weird problem greater than or less than the amount of time that would have been spent porting the classic ASP pages to ASP.NET?"
Tuesday, August 22, 2006 10:46:31 PM UTC
Ooh, fun corner case. Because of course the rule is that when calling into a .NET object via COM interop, the newest version of the runtime is always loaded. It just so happens to be running under IIS this time, I guess.

>If the anonymous identity was left as the default IUSR_xxxx,
>the CLR loaded is still 1.1

Now that's pretty weird. Any chance the IUSR_ account doesn't have permissions to the .NET 2.0 directories? Probably not.
Tuesday, August 22, 2006 10:58:22 PM UTC
Oh, and from the MS docs:

"COM applications hosted by an extensible host, such as Microsoft Internet Explorer or Microsoft Office cannot control which version of the runtime is loaded."

I suspect IIS qualifies here.
Wednesday, August 23, 2006 12:03:50 AM UTC
> The Classic ASP page is hit first, and apparently can't control the CLR version it gets!

Kevin Dente got it. We ran into the same thing when hosting .NET controls in IE6.*

When the entity loading the .NET control (IE6) is unmanaged code, you ALWAYS get the latest version of the runtime installed on the machine, whatever that happens to be.

* In case you were wondering, this is not a good idea. What can I say. It was 2003. We were drunk on .NET power.
Wednesday, August 23, 2006 12:24:08 AM UTC
I know this isn't a real fix on the problem, but is there a reason you can't simply split the asp files and the asp.net files into seperate directories? You can then apply a seperate application pool to each directory. Since application pools use seperate threads and, thus, seperate CLR instances, you should therefore be fine.
John Christensen
Wednesday, August 23, 2006 12:28:52 AM UTC
We can't split the ASP.NET and ASP app up, because the ASP app is "officially in charge" while the ASP.NET app is actually a "plug-in"...

Sigh...all good points and good info. FAD.
Scott Hanselman
Wednesday, August 23, 2006 2:30:30 AM UTC
Scott, we ran into the same problem when a VB6 client called a .NET component (that was built under v1.1) on a machine that had v2.0 and v1.1 of the framework. How we fixed it? We the requiredRuntime element within our app.config for the VB6 client. This fixed the issue and appears to be the "easiest" workaround. You can see my full explanation here: http://www.lozanotek.com/archive/2006/03/14/7928.aspx
Wednesday, August 23, 2006 4:07:39 AM UTC
Hi Scotte,
Did you tried forcing your application to use particular runtime only, by below element.
I think you can even define machine wide policy file for this.

< requiredRuntime version="runtime version" safemode="true|false"/ >
dhananjay123@hotmail.com
Wednesday, August 23, 2006 4:50:23 PM UTC
We've had the same issue. We have tried the requiredRuntime option in the web.config file and that hasn't worked. We are getting ready to call PSS to see if there is anything that can be done.

Here's a thread where we've talked about different things to try. None have worked for us, but just in case they are helpful for someone else troubleshooting:
http://forums.iis.net/thread/1363620.aspx

If we come up with a solution, I'll post back in the comments here.
Mick
Friday, August 25, 2006 12:07:52 PM UTC
You could call CorBindToRuntimeEx to force a load of .NET 1.1. You could either do this in a C++ COM component, which you could create first thing on your .ASP pages, or in a tiny ISAPI filter.

Another way: Have the first request to an .ASP page redirect to an .ASPX page (to force a load of .NET 1.1) and then back to the .ASP page. You could make sure that this only happens once via a flag stored in .ASP application state.
Greg
Wednesday, August 30, 2006 1:30:50 PM UTC
Greg is right above. I called PSS and they gave me an ISAPI dll that does just what Greg mentioned. Basically, it loads when the web app first loads and makes sure the 1.1 framework is loaded. Here is the function by itself:

BOOL CNativeISAPIFilter::GetFilterVersion(PHTTP_FILTER_VERSION pVer)
{
LPWSTR pszVer = L"v1.1.4322";
LPWSTR pszFlavor = L"wks";
ICorRuntimeHost *pHost = NULL;

HRESULT hr = CorBindToRuntimeEx(
//version
pszVer,

// svr or wks
pszFlavor,

//domain-neutral"ness" and gc settings - see below.
STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN | STARTUP_CONCURRENT_GC,

CLSID_CorRuntimeHost,
IID_ICorRuntimeHost,
(void **)&pHost);


// Call default implementation for initialization
CHttpFilter::GetFilterVersion(pVer);

// Clear the flags set by base class
pVer->dwFlags &= ~SF_NOTIFY_ORDER_MASK;

// Set the flags we are interested in
pVer->dwFlags |= SF_NOTIFY_SECURE_PORT | SF_NOTIFY_NONSECURE_PORT | SF_NOTIFY_END_OF_NET_SESSION | SF_NOTIFY_END_OF_REQUEST;

// Set Priority
pVer->dwFlags |= SF_NOTIFY_ORDER_HIGH;

// Load description string
TCHAR sz[SF_MAX_FILTER_DESC_LEN+1];

ISAPIVERIFY(::LoadString(AfxGetResourceHandle(),
IDS_FILTER, sz, SF_MAX_FILTER_DESC_LEN));
_tcscpy(pVer->lpszFilterDesc, sz);

return TRUE;
}

They sent me a full solution file if anyone is interested. Scott, I can send it to you if you want to link to it on this blog entry.
Mick
Wednesday, August 30, 2006 5:41:22 PM UTC
Forgot to fix one thing above. This line: LPWSTR pszFlavor = L"wks"; should be LPWSTR pszFlavor = L"svr"; if you are using a multiprocessor server.
Mick
Wednesday, August 30, 2006 10:22:13 PM UTC
Nice stuff Mick. I'll update the post and make a new one as well. That's kind of what I was thinking but you've saved us a day. I have the skeleton of an ISAPI filter and I was going to take Greg's advice. A filter is the first place to jump in an influence, and GetFilterVersion is only called once, so it's a clever hack.
Scott Hanselman
Thursday, September 07, 2006 4:15:05 PM UTC
How about adding a "dllhost.exe.config" file into your c:\winnt\system32 directory? Classic ASP pages will run using the DLLHost if the protection is Medium or High. I think this also applies to IIS5+.

Contents of "dllhost.exe.config":

<?xml version="1.0"?>
<configuration>
<startup>
<requiredRuntime version="v1.1.4322"/>
</startup>
</configuration>

In IIS6.0, you can set different Application Pools, although I think for classic ASP pages, the DLLHost.exe.config will still work? I know the processModel is different in IIS6.0, so I'm not entirely sure about this solution under IIS6.0...
Mike
Thursday, September 07, 2006 4:34:04 PM UTC
The dllhost thing will work, but it affects EVERYTHING that uses
dllhost, which isn't cool. It also doesn't allow for mixing 2.0 and 1.1.

Here's how we solved it:
http://www.hanselman.com/blog/SOLVEDHowToForceIISToLoadACertainVersionOfTheNETCLR.aspx
Thursday, September 07, 2006 7:16:21 PM UTC
True. But, if you have classic ASP using some COM Interop DLL's built with Framework 1.1, then only those older classic ASP should be using dllhost. Or, is this assumption incorrect? If I have ASP.NET sites running on Framework 2.0, won't those still use 2.0, or are those then forced to use 1.1? Jeeez, I guess they are...
Mike
Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.