Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exception occurred while constructing custom validator attribute. #966

Closed
hubuk opened this issue Apr 9, 2018 · 6 comments · Fixed by #1659
Closed

Exception occurred while constructing custom validator attribute. #966

hubuk opened this issue Apr 9, 2018 · 6 comments · Fixed by #1659

Comments

@hubuk
Copy link
Contributor

hubuk commented Apr 9, 2018

Steps to reproduce

Run run.ps1
Source code available also here:
https://github.com/hubuk/PSSAReproduction

run.ps1

$env:PSModulePath = "$(Resolve-Path 'Modules');$env:PSModulePath"
Invoke-Analysis

Modules\Common\Common.psm1

function Invoke-WithValidation {
    [CmdletBinding()]
    param (
        [ValidateSomethingAttribute()]
        $Path)
}
class ValidateSomethingAttribute : System.Management.Automation.ValidateArgumentsAttribute {
    [void] Validate([object]$arguments, [System.Management.Automation.EngineIntrinsics]$engineIntrinsics) {
    }
}
Export-ModuleMember -Function '*'

Modules\Main\Main.psm1

function Invoke-Analysis {
    Invoke-ScriptAnalyzer (Join-Path $PSScriptRoot 'Main.psm1')
    Invoke-WithValidation
}
Export-ModuleMember -Function '*'

Expected behavior

No exception shall be reported.

Actual behavior

Exception thrown:

Invoke-ScriptAnalyzer : The following exception occurred while constructing the attribute "ValidateS
omethingAttribute": "There is no Runspace available to run scripts in this thread. You can provide o
ne in the DefaultRunspace property of the System.Management.Automation.Runspaces.Runspace type. The 
script block you attempted to invoke was: "
At F:\GitHub\Leet\PSSAReproduction\Modules\Main\Main.psm1:2 char:5
+     Invoke-ScriptAnalyzer (Join-Path $PSScriptRoot 'Main.psm1')

Environment data

> $PSVersionTable
Name                           Value
----                           -----
PSVersion                      6.0.1
PSEdition                      Core
GitCommitId                    v6.0.1
OS                             Microsoft Windows 10.0.16299
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0
> (Get-Module -ListAvailable PSScriptAnalyzer).Version | ForEach-Object { $_.ToString() }
1.16.1
@LaurentDardenne
Copy link

The exception is different in 5.1

$PSVersionTable

Name                           Value
----                           -----
PSVersion                      5.1.14409.1012
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0, 5.0, 5.1.14409.1012}
BuildVersion                   10.0.14409.1012
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1


$error[0]|select *


writeErrorStream      : True
PSMessageDetails      :
Exception             : System.Management.Automation.RuntimeException: The following exception occurred while
                        constructing the attribute "ValidateSomethingAttribute": "La valeur ne peut pas être null.
                        Nom du paramètre : key" ---> System.ArgumentNullException: La valeur ne peut pas être null.
                        Nom du paramètre : key
                           à System.ThrowHelper.ThrowArgumentNullException(ExceptionArgument argument)
                           à System.Runtime.CompilerServices.ConditionalWeakTable`2.TryGetValue(TKey key, TValue&
                        value)
                           à ValidateSomethingAttribute..ctor()
                           à CallSite.Target(Closure , CallSite , Object )
                           à System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
                           --- Fin de la trace de la pile d'exception interne ---
                           à System.Management.Automation.Language.Compiler.GetAttribute(AttributeAst attributeAst)
                           à System.Management.Automation.Language.Compiler.GetRuntimeDefinedParameter(ParameterAst
                        parameterAst, Boolean& customParameterSet, Boolean& usesCmdletBinding)
                           à System.Management.Automation.Language.Compiler.GetParameterMetaData(ReadOnlyCollection`1
                        parameters, Boolean automaticPositions, Boolean& usesCmdletBinding)
                           à System.Management.Automation.CompiledScriptBlockData.InitializeMetadata()
                           à System.Management.Automation.CompiledScriptBlockData.GetAttributes()
                           à Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.UseShouldProcessCorrectly.Support
                        sShouldProcess(String cmdName)
                           à Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.UseShouldProcessCorrectly.CheckFo
                        rSupportShouldProcess()
                           à Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.UseShouldProcessCorrectly.<Analyz
                        eScript>d__7.MoveNext()
                           à System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
                           à System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
                           à Microsoft.Windows.PowerShell.ScriptAnalyzer.ScriptAnalyzer.<>c__DisplayClass78_0.<AnalyzeS
                        yntaxTree>b__1()
TargetObject          : C:\Users\Laurent\Downloads\PSSAReproduction-master\Modules\Main\Main.psm1
CategoryInfo          : InvalidOperation: (C:\Users\Lauren...\Main\Main.psm1:String) [Invoke-ScriptAnalyzer],
                        RuntimeException
FullyQualifiedErrorId : RULE_ERROR,Microsoft.Windows.PowerShell.ScriptAnalyzer.Commands.InvokeScriptAnalyzerCommand
ErrorDetails          :
InvocationInfo        : System.Management.Automation.InvocationInfo
ScriptStackTrace      : at Invoke-Analysis, C:\Users\Laurent\Downloads\PSSAReproduction-master\Modules\Main\Main.psm1:
                        line 5
                        at <ScriptBlock>, C:\Users\Laurent\Downloads\PSSAReproduction-master\run.ps1: line 2
                        at <ScriptBlock>, <No file>: line 1
PipelineIterationInfo : {0, 1}

The code works if we use a C# class :

.Modules\Common\Common.psm1
Add-type @'
public class ValidateSomethingAttribute : System.Management.Automation.ValidateArgumentsAttribute
{
    protected override void Validate(object arguments, System.Management.Automation.EngineIntrinsics engineIntrinsics)
    {
    }
}
'@

function Invoke-WithValidation {
    [CmdletBinding()]

    param (
        [ValidateSomethingAttribute()]
        $Path)
}

# class ValidateSomethingAttribute : System.Management.Automation.ValidateArgumentsAttribute
# {
#     [void] Validate([object]$arguments, [System.Management.Automation.EngineIntrinsics]$engineIntrinsics)
#     {
#     }
# }

Export-ModuleMember -Function '*'

@bergmeister
Copy link
Collaborator

bergmeister commented Apr 11, 2018

Thanks for reporting this and the detailed repro steps and feedback. I can confirm this reproes as well in the latest development version in Windows PowerShell and PowerShell Core but in both cases only when run.ps1 is being executed the first time or if Invoke-ScriptAnalyzer . -Recurse was not called before. I could pin it down to where it happens but it needs more time to be looked at why it happens and what the best way to resolve it is. I think it is a multi-threading issue originating from Helper.Instance.GetCommandInfoLegacy that proved the cmdInfo object. cc @JamesWTruher
image

@bergmeister
Copy link
Collaborator

bergmeister commented Apr 22, 2018

A peak into the currently running threads just before the debugger hits the call funcInfo.ScriptBlock.Atrributes that throws the exception shows that the underlying issue is the GetCommandInfoLegacy method being called by other rules (in other threads), which internally holds a lock to the commandInfo cache and therefore prevents indirectly access to the Attributes member. We need to revisit locking (cloning the object does not seem to be easily possible).
image

@bergmeister
Copy link
Collaborator

@JamesWTruher What do you think about using the ReaderWriterLockSlim class for locking instead of the simple lock (which uses the Monitor class under the hood). This should improve behaviour since most operations are only reading.
In this case (I think) the issue seems to be though that we access an element of the commandInfo cache list indirectly (and potentially the property call also tries to perform some action that then fails) because we get a pointer to it from the call to GetCommandInfoCacheLegacy, which indicates to me that we also need to use then a thread-safe collection type as well or have a read-wrapper using ReaderWriterLockSlim when accessing the locked resource indirectly as in this case outside the Helper class. What do you think?

@bergmeister
Copy link
Collaborator

bergmeister commented May 9, 2018

All my previous analysis is still true in terms of improvements to be made but I think I could (finally) figure out what happens in this case:
PSSA analysis scripts file by file serially but for each file it kicks off each rule in a separate thread (but waits until all rules are finished until continuing to the next file). So far so good. What happens is:
1.) PSSA analyzes Main.ps1 first and one of the Rules is UseSupportShouldProcess, this rule tries to look up the Attribute of the method call to Invoke-WithValidation in Main.ps but this function and its custom attribute are defined in Common.ps1. PSSA initializes some of its variable/command cache before it starts its work and it happens that it knows about the function and attribute being defined but not the custom attribute definition yet. If one replaced ValidateSomethingAttribute with a common Attribute like ValidateNotNull(), then the exception would not occur (or if Common.psm1 gets analyzed beforehand)
Therefore we could catch this specific exception (because the definition could always be outside the files being analysed). I am not sure if it is possible to get to the definition beforehand without doing a lot of work.

By doing some analysis in PowerShell, the following works from the command line:

$env:PSModulePath = "$(Resolve-Path 'Modules');$env:PSModulePath"
$c=gcm Invoke-WithValidation
$c.ScriptBlock.Attributes

Therefore I think (and I got more confirmed when debugging) that in the call to GetCommandInfoInternal that calls PowerShell.Create() that this new instance does not full sessionstate information because the returned object already has this RuntimeException on the Attributes property here. I tried to repro this by calling those apis in powershell itself but it did not repro, therefore it does not seem to be a bug in PowerShell itself.

@et1975
Copy link

et1975 commented Jun 1, 2021

Please specify the released/target version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants