Powershell - Assembly binding redirect NOT found in application configuration file

12

Got a problem here...

I have got a Powershell CmdLet that works when running in 32-bit mode and fails in 64-bit mode. Question is what the cause is and how it can be fixed.

Situation

Powershell CmdLet that references 'OutlookHelper.Common.dll'. Newest version is 2.0.0.0 The CmdLet also uses logging and references 'Logging.dll'.
Logging.dll also references 'OutlookHelper.Common.dll', only was compiled against version 1.0.0.0.

How I made it work, this works partially

Using an assembly binding redirect in the application configuration file of Powershell:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup useLegacyV2RuntimeActivationPolicy="true"> 
    <supportedRuntime version="v4.0.30319"/> 
    <supportedRuntime version="v2.0.50727"/> 
  </startup> 
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="OutlookHelper.Common" publicKeyToken="5e4553dc0df45306"/>
        <bindingRedirect oldVersion="1.0.0.0" newVersion="2.0.0.0"/>
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

Powershell 32-bits works just fine

When running on a 64-bits machine, using 'Windows Powershell (x86)' it works. The assembly manager finds an assembly binding redirect:

The operation was successful.
Bind result: hr = 0x0. The operation completed successfully.

Assembly manager loaded from:  C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll
Running under executable  C:\Windows\syswow64\Windowspowershell\v1.0\powershell.exe
--- A detailed error log follows. 

=== Pre-bind state information ===
LOG: User = MYDOMAIN\testuser
LOG: DisplayName = OutlookHelper.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=5e4553dc0df45306
 (Fully-specified)
LOG: Appbase = file:///C:/Windows/syswow64/Windowspowershell/v1.0/
LOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = powershell.exe
Calling assembly : OutlookHelper.Data.Common, Version=1.0.5295.26925, Culture=neutral, PublicKeyToken=null.
===
LOG: This bind starts in LoadFrom load context.
WRN: Native image will not be probed in LoadFrom context. Native image will only be probed in default load context, like with Assembly.Load().
LOG: Using application configuration file: C:\Windows\syswow64\Windowspowershell\v1.0\powershell.exe.Config
LOG: Using host configuration file: 
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config.
LOG: Redirect found in application configuration file: 1.0.0.0 redirected to 2.0.0.0.
LOG: Post-policy reference: OutlookHelper.Common, Version=2.0.0.0, Culture=neutral, PublicKeyToken=5e4553dc0df45306
LOG: GAC Lookup was unsuccessful.
LOG: Attempting download of new URL file:///C:/Windows/syswow64/Windowspowershell/v1.0/OutlookHelper.Common.DLL.
LOG: Attempting download of new URL file:///C:/Windows/syswow64/Windowspowershell/v1.0/OutlookHelper.Common/OutlookHelper.Common.DLL.
LOG: Attempting download of new URL file:///C:/Windows/syswow64/Windowspowershell/v1.0/OutlookHelper.Common.EXE.
LOG: Attempting download of new URL file:///C:/Windows/syswow64/Windowspowershell/v1.0/OutlookHelper.Common/OutlookHelper.Common.EXE.
LOG: Attempting download of new URL file:///D:/SampleApps/_Common/Bin/Outlook.Extensions.Sample/OutlookHelper.Common.DLL.
LOG: Assembly download was successful. Attempting setup of file: D:\SampleApps\_Common\Bin\Outlook.Extensions.Sample\OutlookHelper.Common.dll
LOG: Entering run-from-source setup phase.
LOG: Assembly Name is: OutlookHelper.Common, Version=2.0.0.0, Culture=neutral, PublicKeyToken=5e4553dc0df45306
LOG: Where-ref bind Codebase does not match what is found in default context. Keep the result in LoadFrom context.
LOG: Binding succeeds. Returns assembly from D:\SampleApps\_Common\Bin\Outlook.Extensions.Sample\OutlookHelper.Common.dll.
LOG: Assembly is loaded in LoadFrom load context.

Here is what Powershell says about the assembly identity:

Windows PowerShell (x86)
Copyright (C) 2009 Microsoft Corporation. All rights reserved.

PS C:\Users\testuser> ([xml](gc $([System.AppDomain]::CurrentDomain.SetupInformation.ConfigurationFile))).configuratio
n.runtime.assemblyBinding.dependentAssembly.assemblyIdentity

name                                                        publicKeyToken
----                                                        --------------
OutlookHelper.Common                                        5e4553dc0df45306

PS C:\Users\testuser>

This is where the trouble starts...

When running on a 64-bits machine, using 'Windows Powershell' it does not work. The assembly manager does NOT find the assembly binding redirect:

The operation failed.
Bind result: hr = 0x80070002. The system cannot find the file specified.

Assembly manager loaded from:  C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clr.dll
Running under executable  C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe
--- A detailed error log follows. 

=== Pre-bind state information ===
LOG: User = MYDOMAIN\testuser
LOG: DisplayName = OutlookHelper.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=5e4553dc0df45306
 (Fully-specified)
LOG: Appbase = file:///C:/WINDOWS/system32/WindowsPowerShell/v1.0/
LOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = powershell.exe
Calling assembly : OutlookHelper.Data.Common, Version=1.0.5295.26925, Culture=neutral, PublicKeyToken=null.
===
LOG: This bind starts in LoadFrom load context.
WRN: Native image will not be probed in LoadFrom context. Native image will only be probed in default load context, like with Assembly.Load().
LOG: Using application configuration file: C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe.Config
LOG: Using host configuration file: 
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework64\v4.0.30319\config\machine.config.
LOG: Post-policy reference: OutlookHelper.Common, Version=1.0.0.0, Culture=neutral, PublicKeyToken=5e4553dc0df45306
LOG: The same bind was seen before, and was failed with hr = 0x80070002.
ERR: Unrecoverable error occurred during pre-download check (hr = 0x80070002).

Here is what Powershell says about the assembly identity:

Windows PowerShell
Copyright (C) 2009 Microsoft Corporation. All rights reserved.

PS C:\Users\testuser> ([xml](gc $([System.AppDomain]::CurrentDomain.SetupInformation.ConfigurationFile))).configuratio
n.runtime.assemblyBinding.dependentAssembly.assemblyIdentity
PS C:\Users\ccontent01>

When I let Powershell get the contents of its own application configuration file I get the following output:

Windows PowerShell
Copyright (C) 2009 Microsoft Corporation. All rights reserved.

PS C:\Users\testuser> gc C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe.Config
<?xml version="1.0"?>
<configuration>
    <startup useLegacyV2RuntimeActivationPolicy="true">
        <supportedRuntime version="v4.0.30319"/>
        <supportedRuntime version="v2.0.50727"/>
    </startup>
</configuration>
PS C:\Users\testuser>

What I tried...

  • Check whether 'Specific Version' is set to 'false' -> this is the case.
  • Deleted and re-added the application configuration file -> did not fix it.
  • Started with a new application configuration file in the SysWOW64 folder -> did not fix it.
  • Double checked the contents of the files that are loaded (powershell.exe.Config and machine.config) -> they are the same.

My guess

  • Assembly manager cannot find the assembly redirect binding.

Any solutions?

  • Why does the Fusion log for the 64-bits instance not mention something like 'Redirect found in application configuration file: 1.0.0.0 redirected to 2.0.0.0.'?
  • What can be the cause of all this?
  • Can you think of any solutions?
.net
powershell
32bit-64bit
assembly-binding-redirect
asked on Stack Overflow Oct 30, 2014 by mathijsuitmegen • edited Nov 4, 2014 by mathijsuitmegen

3 Answers

9

Not 100% related to 32/64-bit issue, however if someone is interested in working assembly redirect solution please have a look here Powershell config assembly redirect.

You can do custom assembly redirect using PowerShell code like

$FSharpCore = [reflection.assembly]::LoadFrom($PSScriptRoot + "\bin\LIBRARY\FSharp.Core.dll") 

$OnAssemblyResolve = [System.ResolveEventHandler] {
  param($sender, $e)

  # from:FSharp.Core, Version=4.3.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  # to:  FSharp.Core, Version=4.4.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
  if ($e.Name -eq "FSharp.Core, Version=4.3.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a") { return $FSharpCore }

  foreach($a in [System.AppDomain]::CurrentDomain.GetAssemblies())
  {
    if ($a.FullName -eq $e.Name)
    {
      return $a
    }
  }
  return $null
}

[System.AppDomain]::CurrentDomain.add_AssemblyResolve($OnAssemblyResolve)

I am first loading the correct version of FSharp.Core from somewhere as the version in the GAC is old (I guess this might be your case too)

answered on Stack Overflow Oct 23, 2015 by davidpodhola • edited Jun 5, 2020 by davidpodhola
6

Based on @davidpodhola's extremely helpful answer, I started putting something like this in my psm1 module files. If your newer assemblies are already loaded (by Import-Module for instance), this should work:

if (!("Redirector" -as [type]))
{
$source = 
@'
using System;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

public class Redirector
{
    public readonly string[] ExcludeList;

    public Redirector(string[] ExcludeList = null)
    {
        this.ExcludeList  = ExcludeList;
        this.EventHandler = new ResolveEventHandler(AssemblyResolve);
    }

    public readonly ResolveEventHandler EventHandler;

    protected Assembly AssemblyResolve(object sender, ResolveEventArgs resolveEventArgs)
    {
        Console.WriteLine("Attempting to resolve: " + resolveEventArgs.Name); // remove this after its verified to work
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            var pattern  = "PublicKeyToken=(.*)$";
            var info     = assembly.GetName();
            var included = ExcludeList == null || !ExcludeList.Contains(resolveEventArgs.Name.Split(',')[0], StringComparer.InvariantCultureIgnoreCase);

            if (included && resolveEventArgs.Name.StartsWith(info.Name, StringComparison.InvariantCultureIgnoreCase))
            {
                if (Regex.IsMatch(info.FullName, pattern))
                {
                    var Matches        = Regex.Matches(info.FullName, pattern);
                    var publicKeyToken = Matches[0].Groups[1];

                    if (resolveEventArgs.Name.EndsWith("PublicKeyToken=" + publicKeyToken, StringComparison.InvariantCultureIgnoreCase))
                    {
                        Console.WriteLine("Redirecting lib to: " + info.FullName); // remove this after its verified to work
                        return assembly;
                    }
                }
            }
        }

        return null;
    }
}
'@

    $type = Add-Type -TypeDefinition $source -PassThru 
}

#exclude all powershell related stuff, not sure this strictly necessary
$redirectExcludes = 
    @(
        "System.Management.Automation", 
        "Microsoft.PowerShell.Commands.Utility",
        "Microsoft.PowerShell.Commands.Management",
        "Microsoft.PowerShell.Security",
        "Microsoft.WSMan.Management",    
        "Microsoft.PowerShell.ConsoleHost",
        "Microsoft.Management.Infrastructure",
        "Microsoft.Powershell.PSReadline",
        "Microsoft.PowerShell.GraphicalHost"
        "System.Management.Automation.HostUtilities",

        "System.Management.Automation.resources",
        "Microsoft.PowerShell.Commands.Management.resources",
        "Microsoft.PowerShell.Commands.Utility.resources",
        "Microsoft.PowerShell.Security.resources",
        "Microsoft.WSMan.Management.resources",
        "Microsoft.PowerShell.ConsoleHost.resources",
        "Microsoft.Management.Infrastructure.resources",
        "Microsoft.Powershell.PSReadline.resources",
        "Microsoft.PowerShell.GraphicalHost.resources",
        "System.Management.Automation.HostUtilities.resources"
    )
try
{
    $redirector = [Redirector]::new($redirectExcludes)
    [System.AppDomain]::CurrentDomain.add_AssemblyResolve($redirector.EventHandler)
}
catch
{
    #.net core uses a different redirect method
    write-warning "Unable to register assembly redirect(s). Are you on ARM (.Net Core)?"
}

Update: Powershell appears to have a bug where simply registering an assembly resolve scriptblock can cause a StackOverflowException when calling some commands like Out-GridView. I updated the code to use a version compiled with Add-Type that seems to resolve the issue.

answered on Stack Overflow Nov 2, 2016 by Tim • edited May 3, 2017 by Tim
3

On a 64-bit machine there are two configuration files:

C:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe.Config
C:\Windows\syswow64\Windowspowershell\v1.0\powershell.exe.Config

Have you edited both of them on the 64-bit machine?

On 64-bit versions of Windows. 32 bit processes (like notepad++) are transparently redirected from C:\WINDOWS\System32 to C:\WINDOWS\SysWOW64 by the OS.

You will need to make sure you edit both files using a 64-bit text editor like the builtin notepad.exe. This will guarantee you do not suffer from this subtle issue, which can cause confusion.

answered on Stack Overflow Nov 3, 2014 by DanL • edited Nov 4, 2014 by mathijsuitmegen

User contributions licensed under CC BY-SA 3.0