August Madness - A tale of Classic ASP, Two CLRs, IIS, and COM Interop
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)
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.
>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.
"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.
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.
Sigh...all good points and good info. FAD.
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"/ >
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:
If we come up with a solution, I'll post back in the comments here.
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.
BOOL CNativeISAPIFilter::GetFilterVersion(PHTTP_FILTER_VERSION pVer)
LPWSTR pszVer = L"v1.1.4322";
LPWSTR pszFlavor = L"wks";
ICorRuntimeHost *pHost = NULL;
HRESULT hr = CorBindToRuntimeEx(
// svr or wks
//domain-neutral"ness" and gc settings - see below.
STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN | STARTUP_CONCURRENT_GC,
// Call default implementation for initialization
// 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
IDS_FILTER, sz, SF_MAX_FILTER_DESC_LEN));
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.
Contents of "dllhost.exe.config":
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...
dllhost, which isn't cool. It also doesn't allow for mixing 2.0 and 1.1.
Here's how we solved it:
Comments are closed.
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?"