Search code examples
c#dllwow64

WOW64 Program Files redirection not working


I have a managed application with 32-bit and 64-bit versions which must both be able to run on the same machine, with a dependency on unmanaged DLLs of both bitnesses with identical names (namely, ICU 4.2).

So, I decided to put both versions of ICU in their matching Program Files, relying on the WOW64 filesystem redirection to do the heavy lifting: The 64-bit version in C:\Program Files\ICUBins and the 32-bit version in C:\Program Files (x86)\ICUBins; and the former was added to the PATH variable.

Except... it doesn't work. Both versions of my program end up using the 64-bit version of the unmanaged DLLs, and I don't understand why the attempt to use C:\Program Files\ICUBins by the 32-bit process isn't redirected to C:\Program Files (x86)\ICUBins.

Edited to add:

Here is a minimal code example that reproduces the problem (but since the wrapper code was huge, instead of loading the DLL, it merely checks its bitness). Note that it uses obsolete functions to check for WOW64 because it was initially created for .Net 2.0:

using System;

namespace SearchICU
{
    /// <summary>
    /// Methods for getting the version and date of the assembly that calls them.
    /// </summary>
    public static class BitnessHelper
    {
        public enum Machine : short
        {
            x86=0x14C,
            Alpha=0x184,
            ARM=0x1C0,
            MIPS16R3000=0x162,
            MIPS16R4000=0x166,
            MIPS16R10000=0x168,
            PowerPCLE=0x1F0,
            PowerPCBE=0x1F2,
            Itanium=0x200,
            MIPS16=0x266,
            Alpha64=0x284,
            MIPSFPU=0x366,
            MIPSFPU16=0x466,
            x64=unchecked((short)0x8664),
        }

        public static Machine RetrieveMachine(string filePath)
        {
            if(string.IsNullOrEmpty(filePath)) { throw new ArgumentNullException("filePath"); }

            const int c_PeHeaderOffsetOffset = 60; //Within whole file/IMAGE_DOS_HEADER structure. Equal to 0x003C
            const int c_MachineOffset = 4; //Within IMAGE_NT_HEADERS

            //Just read "enough" of the file: In modern PE files, the IMAGE_NT_HEADERS structure should never start past 2KiB anyway.
            byte[] b = new byte[2048];
            System.IO.Stream s = null;

            try
            {
                s = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read);
                s.Read(b, 0, 2048);
            }
            finally
            {
                if(s != null)
                {
                    s.Close();
                }
            }

            //First check the MZ header (IMAGE_DOS_HEADER)'s magic.
            short mzMagic = ReadInt16LE(b, 0);
            if(mzMagic != 0x5A4D) //"MZ" in little-endian
                throw new BadImageFormatException("File does not start with MZ header.");

            //Get its "next header offset" value and check the PE header (IMAGE_NT_HEADERS)'s magic.
            int peHeaderOffset = ReadInt32LE(b, c_PeHeaderOffsetOffset);
            int peMagic = ReadInt32LE(b, peHeaderOffset);
            if(peMagic != 0x00004550) //"PE\0\0" in little-endian
                throw new BadImageFormatException("Header pointed by MZ header is not PE.");

            //Read the machine from the PE header (IMAGE_NT_HEADERS).
            //We're still in the bitness-agnostic part (the IMAGE_FILE_HEADER structure).
            short machine = ReadInt16LE(b, peHeaderOffset + c_MachineOffset);
            return (Machine)machine;
        }

        /// <summary>Reads a 16-bit integer as little-endian from a byte array.</summary>
        /// <remarks>Because BitConverter depends on the platform's endianness, and here I need an "always little-endian" code.
        /// Made public because some other code has a need for this.</remarks>
        public static short ReadInt16LE(byte[] bytes, int offset)
        {
            if(bytes==null) { throw new ArgumentNullException("bytes"); }

            ushort ret = 0;
            for(int i=1 ; i>=0 ; i--) { ret <<= 8; ret |= bytes[offset+i]; }
            return unchecked((short)ret);
        }
        /// <summary>Reads a 32-bit integer as little-endian from a byte array.</summary>
        /// <remarks>Because BitConverter depends on the platform's endianness, and here I need an "always little-endian" code.
        /// Made public because some other code has a need for this.</remarks>
        public static int ReadInt32LE(byte[] bytes, int offset)
        {
            if(bytes==null) { throw new ArgumentNullException("bytes"); }

            uint ret = 0;
            for(int i=3 ; i>=0 ; i--) { ret <<= 8; ret |= bytes[offset+i]; }
            return unchecked((int)ret);
        }

        #region Win64/WOW64 methods
        /// <summary>
        /// Win32 function <c>IsWow64Process</c>: Determines whether the specified process is running under WOW64.
        /// </summary>
        /// <param name="hProcess">[in] Process handle with enough access rights.</param>
        /// <param name="Wow64Process">[out] set to <c>true</c> if running under WOW64, <c>false</c> for Win32 and Win64.</param>
        /// <returns><c>true</c> if succeeded.</returns>
        [System.Runtime.InteropServices.DllImport("Kernel32.dll", SetLastError=true)]
        extern static bool IsWow64Process(IntPtr hProcess, ref bool Wow64Process);

        /// <summary>
        /// Wrapper for <c>IsWow64Process</c>, so the calling function may throw <c>SecurityException</c> when calling it
        /// rather than be completely prevented from running by <c>LinkDemand</c>.
        /// </summary>
        static bool CallIsWow64Process(IntPtr hProcess, ref bool Wow64Process)
        {
            //P/Invoke has a LinkDemand for full trust, so this function won't even start
            //if partially trusted.
            return IsWow64Process(hProcess, ref Wow64Process);
        }

        /// <summary>
        /// Wrapper for <c>Process.GetCurrentProcess</c>, so the calling function may throw <c>SecurityException</c> when calling it
        /// rather than be completely prevented from running by <c>LinkDemand</c>.
        /// </summary>
        static IntPtr GetProcessHandle()
        {
            //GetCurrentProcess() has a LinkDemand for full trust, so this function won't even start
            //if partially trusted.
            return System.Diagnostics.Process.GetCurrentProcess().Handle;
        }

        /// <summary>
        /// Wrapper for <c>Marshal.GetLastWin32Error</c>, so the calling function may throw <c>SecurityException</c> when calling it
        /// rather than be completely prevented from running by <c>LinkDemand</c>.
        /// </summary>
        static int CallGetLastWin32Error()
        {
            //GetLastWin32Error() has a LinkDemand for UnmanagedCode, so this function won't even start
            //if partially trusted.
            return System.Runtime.InteropServices.Marshal.GetLastWin32Error();
        }

        /// <summary>
        /// Test whether the current process is running under Win32, Win64 or WOW64.
        /// </summary>
        /// <param name="message">[out] Human-readable message describing the situation.</param>
        /// <returns><c>true</c> if succeeded, <c>false</c> if couldn't determine (security or IsWow64Process failure).</returns>
        /// <exception cref="Exception">For any other error with the P/Invoke call.</exception>
        public static bool TestWow64(out string message)
        {
            //Note on exceptions: Apparently, on a full .Net Framework P/Invoke can throw EntryPointNotFoundException,
            //ExecutionEngineException (if incorrectly declared) or DllNotFoundException.
            //(the former two replaced with MissingMethodException and NotSupportedException on the Compact Framework).
            //Since we're hitting Kernel32.dll, using the correct declaration, and not planning for an embedded version,
            //only EntryPointNotFoundException will be handled here.
            try
            {
                bool isWow64 = false;
                //Call wrapper functions to avoid security exceptions being thrown before the try block.
                if(CallIsWow64Process(GetProcessHandle(), ref isWow64))
                {
                    if(isWow64)
                        message = "Running as a 32-bit process on a Win64 machine.";
                    else if(IntPtr.Size==4)
                        message = "Running on Win32.";
                    else if(IntPtr.Size==8)
                        message = "Running on Win64.";
                    else
                        message = string.Format("Something weird: Not WOW64, but pointer size is {0}.", IntPtr.Size);
                    return true;
                }
                else
                {
                    message = string.Format("IsWow64Process was correctly called, but failed with error {0}", CallGetLastWin32Error());
                    return false;
                }
            }
            catch(EntryPointNotFoundException)
            {
                message = "Running on Win32, WOW64 not supported.";
                return true;
            }
            catch(System.Security.SecurityException)
            {
                message = "Running in a sandbox, process information inaccessible.";
                return false;
            }
            //catch(Exception e)
            //{
            //    log.Warn("IsWow64Process call failed:", e); //test
            //}
        }

        /// <summary>
        /// Wrapper method for determining whether the current process is 64-bit.
        /// Useful for determining which version of a library to load.
        /// </summary>
        public static bool IsWin64
        {
            get { return IntPtr.Size==8; } //In V10, use Environment.Is64BitProcess
        }
        #endregion
    }
}

using System;
using System.IO;

namespace SearchICU
{
    class Program
    {
        static void Main(string[] args)
        {
            string bitness;
            if(BitnessHelper.TestWow64(out bitness)) { Console.WriteLine(bitness); }
            string icuDir = FindDirInPath(new string[] { "icudt42.dll", "icuuc42.dll", "icuin42.dll" });
            if(icuDir==null)
            {
                Console.WriteLine("ICU DLLs not found in PATH.");
                return;
            }

            Console.WriteLine("ICU DLLs found in PATH:{1}\t{0}", icuDir, Environment.NewLine);

            string dllPath = Path.Combine(icuDir, "icuin42.dll");
            BitnessHelper.Machine machine = BitnessHelper.RetrieveMachine(dllPath);
            switch(machine)
            {
            case BitnessHelper.Machine.x86:
                Console.WriteLine("DLL in path is 32-bit DLL.");
                break;
            case BitnessHelper.Machine.x64:
                Console.WriteLine("DLL in path is 64-bit DLL.");
                break;
            default:
                Console.WriteLine("DLL in path is unknown (machine={0}).", machine);
                break;
            }
        }

        public static string FindDirInPath(string[] filesToFind)
        {
            if(filesToFind==null || filesToFind.Length==0)
                throw new ArgumentException("filesToFind must be a non-empty array of file names.", "filesToFind");

            string pathEnvVariable = Environment.GetEnvironmentVariable("PATH");
            if(!string.IsNullOrEmpty(pathEnvVariable))
            {
                foreach(string pathDirectory in pathEnvVariable.Split(";".ToCharArray(), StringSplitOptions.RemoveEmptyEntries))
                {
                    bool allFound = true;
                    foreach(string fileName in filesToFind)
                    {
                        string filePath = Path.Combine(pathDirectory, fileName);
                        if(!File.Exists(filePath))
                        {
                            allFound = false;
                            break;
                        }
                    }
                    if(allFound)
                        return pathDirectory;
                }
            }
            return null;
        }
    }
}

When I run the 32-bit version of this program, the output is thus:

Running as a 32-bit process on a Win64 machine.
ICU DLLs found in PATH:
        C:\Program Files\ICUBins
DLL in path is 64-bit DLL.

Solution

  • I think I have found the problem, and the problem is me.

    More exactly, in assuming there was a Program Files redirection in the first place. But nothing in the documentation seems to say this is actually the case: It appears WOW64 doesn't redirect Program Files, but merely changes what %ProgramFiles% points to. And to add insult to injury, I can't use %ProgramFiles% in the system PATH to correct this.

    So either I'll have to set the environment variables per-user (and in the case of websites, per application pool) or I'll have to modify the program itself to properly massage the PATH before trying to access the DLL.

    Don't do this, PATH is already excessively fugly by itself, deploy the DLLs you need in the same directory as the client app

    Thanks, but alas, in my urge to reduce the problem to a minimal example, I neglected to mention I have both a "real" program and an ASP.Net website (and deploying DLLs in the app's directory doesn't work in the latter case). Also, for completeness's sake, the "real world" code uses a C++/CLI wrapper assembly, and it's that assembly that is dynamically loaded, rather than the ICU DLL ("statically" dynamically linked to the C++/CLI wrapper).

    Edit: In the end, I added this to my code:

    /// <summary>
    /// Enumerates the paths in the <c>PATH</c> environment variable, looking for <paramref name="filesToFind"/>,
    /// then checks their bitness and if it's a conflicting bitness in one of the Program Files directories,
    /// looks into the other Program File directory and updates <c>PATH</c> in consequence when successful.
    /// </summary>
    /// <param name="filesToFind">[in] File names.</param>
    /// <param name="oldPath">[out] Old value of <c>PATH</c>. This parameter is always set.</param>
    /// <param name="foundPath">[out] Directory in which the files were found, or <c>null</c> if they weren't.</param>
    /// <returns><c>true</c> if the <c>PATH</c> environment variable was modified.</returns>
    public static bool FindDirInPathAndUpdatePathWithBitness(string[] filesToFind, out string oldPath, out string foundPath)
    {
        if(filesToFind==null || filesToFind.Length==0)
            throw new ArgumentException("filesToFind must be a non-empty array of file names.", "filesToFind");
    
        string pathEnvVariable = Environment.GetEnvironmentVariable("PATH");
        oldPath = pathEnvVariable;
        foundPath = null;
    
        if(!string.IsNullOrEmpty(pathEnvVariable))
        {
            string[] pathArray = pathEnvVariable.Split(";".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
            for(int iPath=0 ; iPath<pathArray.Length ; iPath++)
            {
                string pathDirectory = pathArray[iPath];
                bool allFound = true;
                foreach(string fileName in filesToFind)
                {
                    string filePath = Path.Combine(pathDirectory, fileName);
                    if(!File.Exists(filePath))
                    {
                        allFound = false;
                        break;
                    }
                }
    
                //If all files were found in this directory
                if(allFound)
                {
                    //Now the fun begins
                    try
                    {
                        string firstFilePath = Path.Combine(pathDirectory, filesToFind[0]);
                        bool runningWin64 = BitnessHelper.IsWin64;
                        var fileBitness = BitnessHelper.RetrieveMachine(firstFilePath);
                        if(runningWin64 != (fileBitness==BitnessHelper.Machine.x64))
                        {
                            //Bitness conflict detected. Is this directory in %ProgramFiles%?
                            bool bHandled = HandleBitnessConflict(ref pathDirectory, filesToFind[0], runningWin64, fileBitness);
                            if(bHandled)
                            {
                                pathArray[iPath] = pathDirectory;
                                string newPath = string.Join(";", pathArray);
                                Environment.SetEnvironmentVariable("PATH", newPath);
                                return true;
                            }
                            //Otherwise, several possible scenarios:
                            //Remove the path from PATH and keep searching (which requires some bookkeeping),
                            //or just return foundPath as if the check hadn't happened, letting subsequent code throw a BadImageFormatException.
                            //We'll just do the latter, at least for now.
                        }
                    }
                    catch { }
    
                    foundPath = pathArray[iPath];
                    return false;
                }
            }
        }
        return false;
    }
    
    private static bool HandleBitnessConflict(ref string pathDirectory, string firstFileName, bool runningWin64, BitnessHelper.Machine fileBitness)
    {
        //Bitness conflict detected. Is this directory in %ProgramFiles%?
    
        //Bitness-dependent Program Files
        string programFiles = Environment.GetEnvironmentVariable("ProgramFiles");
        //Always points to 32-bit version, if a 64-bit Windows.
        string programFilesX86 = Environment.GetEnvironmentVariable("ProgramFiles(x86)");
        //Always points to 64-bit version, if a 64-bit Windows 7 or greater.
        string programW6432 = Environment.GetEnvironmentVariable("ProgramW6432");
        char[] directoryChars = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
    
        if(string.IsNullOrEmpty(programFilesX86))
            return false; //Not Win64, so there won't be two Program Files directories anyway.
        if(string.IsNullOrEmpty(programW6432))
        {
            //Pre-7 Windows version: Try an heuristic.
            int ix = programFilesX86.IndexOf(" (x86)", StringComparison.OrdinalIgnoreCase);
            if(ix < 0) { return false; } //Heuristic failed.
            string exactSubstring = programFilesX86.Substring(ix, " (x86)".Length);
            programW6432 = programFilesX86.Replace(exactSubstring, "");
            if(!Directory.Exists(programW6432)) { return false; } //Heuristic failed.
        }
    
        if(pathDirectory.StartsWith(programFilesX86) && fileBitness==BitnessHelper.Machine.x86)
        {
            //The file is a 32-bit file in the 32-bit directory in the path;
            //Since there's a conflict, this must mean the current process is 64-bit
            if(!runningWin64) { return false; } //No conflict, no handling.
    
            string directory64 = Path.Combine(programW6432, pathDirectory.Substring(programFilesX86.Length).TrimStart(directoryChars));
            string filePath64 = Path.Combine(directory64, firstFileName);
            if(Directory.Exists(directory64) && File.Exists(filePath64))
            {
                if(BitnessHelper.RetrieveMachine(filePath64) == BitnessHelper.Machine.x64)
                {
                    pathDirectory = directory64;
                    return true;
                }
            }
        }
        else if(pathDirectory.StartsWith(programW6432) && fileBitness==BitnessHelper.Machine.x64)
        {
            //The file is a 64-bit file in the 64-bit directory in the path;
            //Since there's a conflict, this must mean the current process is 32-bit
            if(runningWin64) { return false; } //No conflict, no handling.
    
            string directory32 = Path.Combine(programFilesX86, pathDirectory.Substring(programW6432.Length).TrimStart(directoryChars));
            string filePath32 = Path.Combine(directory32, firstFileName);
            if(Directory.Exists(directory32) && File.Exists(filePath32))
            {
                if(BitnessHelper.RetrieveMachine(filePath32) == BitnessHelper.Machine.x86)
                {
                    pathDirectory = directory32;
                    return true;
                }
            }
        }
    
        return false;
    }