dotnet / fsharp

The F# compiler, F# core library, F# language service, and F# tooling integration for Visual Studio
https://dotnet.microsoft.com/languages/fsharp
MIT License
3.89k stars 783 forks source link

Friend assemblies ("InternalsVisibleTo") can leak via automatic inlining cause runtime error #7422

Open manofstick opened 5 years ago

manofstick commented 5 years ago

Assembly A exposed it's internal to Assembly B via the InternalsVisibleTo attribute. Assembly B provides a simple method to Assembly C, but because it's a simple method, it is inlined by the f# compiler into Assembly C. This results in Runtime error.

Expected behavior

The FSharp compiler shouldn't inline methods that have calls to methods that have been exposed via InternalsVisibleTo.

Actual behavior

Runtime error.

Known workarounds

As per #5178, the use of [<MethodImpl(MethodImplOptions.NoInlining)>] stops the f# compiler from inling the method, but has the side effect that the JIT also stops inlining the call.

Related information

C:> fsc.exe
Microsoft (R) F# Compiler version 10.4.0 for F# 4.6
Copyright (c) Microsoft Corporation. All Rights Reserved.
dsyme commented 5 years ago

I think this is a duplicate of https://github.com/dotnet/fsharp/issues/7110

matthid commented 5 years ago

@dsyme This talks about a runtime error, #7110 about a compile time error. #7110 is arguably by design but this looks like a faulty optimization by the compiler (=bug). @manofstick A sample would obviously help :)

dsyme commented 5 years ago

Ah yes.

The FSharp compiler shouldn't inline methods that have calls to methods that have been exposed via InternalsVisibleTo.

As mentioned in #7110, the intention is that the compiler doesn't inline in this case. So there may be a bug here

manofstick commented 5 years ago

Sample of this bug...

Clone the repository https://github.com/manofstick/Cistern.Linq

Go to branch fsharp_7422

Load solution from src directory. Set Cistern.Linq.Playground.FSharp as the startup project.

Execute and get the following error at runtime:

Unhandled Exception: System.MethodAccessException: Attempt by method 'Program.main(System.String[])' to access method 'Cistern.Linq.ChainLinq.Links.Identity`1<System.Int32>.get_Instance()' failed.
   at Program.main(String[] _arg1) in C:\src\Cistern.Linq\src\Cistern.Linq.Playground.FSharp\Program.fs:line 17

C:\Program Files\dotnet\dotnet.exe (process 7060) exited with code -1073741510.
Press any key to close this window . . .

To workaround, uncomment the [<MethodImpl(MethodImplOptions.NoInlining)>] in on unfold Linq.fs

OkkeHendriks commented 3 years ago

As promised in https://github.com/fscheck/FsCheck/issues/549 I made a simple example repository to reproduce this issue. It is constructed the same way as the scenario which caused this issue in FsCheck.

When looking at the generated IL, it indeed emits the newobj calls instead of call statements as we saw in https://github.com/fscheck/FsCheck/issues/549:

open assembly_A_optimized.referenceModulePublicA
open assembly_B_not_optimized.referenceModulePublicB

[<EntryPoint>]
let main _ =
    makeExceptionA ()
    makeExceptionAggressiveInliningA ()
    makeExceptionNoInliningA ()

    makeExceptionB ()
    makeExceptionAggressiveInliningB ()
    makeExceptionNoInliningB ()

    0

compiles into (optimization on for using program):

// Location: C:\git\inlined_internal\usage_optimized\bin\Debug\netcoreapp3.1\usage_optimized.dll
// Sequence point data from C:\git\inlined_internal\usage_optimized\bin\Debug\netcoreapp3.1\usage_optimized.pdb

.class public abstract sealed auto ansi
  ProgramOptimized
    extends [System.Runtime]System.Object
{
  .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags)
    = (01 00 07 00 00 00 00 00 ) // ........
    // int32(7) // 0x00000007

  .method public static int32
    main(
      string[] _arg1
    ) cil managed
  {
    .entrypoint
    .custom instance void [FSharp.Core]Microsoft.FSharp.Core.EntryPointAttribute::.ctor()
      = (01 00 00 00 )
    .maxstack 8

    // [6 5 - 6 22]
    IL_0000: ldc.i4.0
    IL_0001: brfalse.s    IL_000b
    IL_0003: ldnull
    IL_0004: unbox.any    [FSharp.Core]Microsoft.FSharp.Core.Unit
    IL_0009: br.s         IL_0011
    IL_000b: newobj       instance void [assembly_A_optimized]assembly_A_optimized.referenceModuleInternalA/TestExceptionA::.ctor()
    IL_0010: throw
    IL_0011: pop

    // [7 5 - 7 40]
    IL_0012: ldc.i4.0
    IL_0013: brfalse.s    IL_001d
    IL_0015: ldnull
    IL_0016: unbox.any    [FSharp.Core]Microsoft.FSharp.Core.Unit
    IL_001b: br.s         IL_0023
    IL_001d: newobj       instance void [assembly_A_optimized]assembly_A_optimized.referenceModuleInternalA/TestExceptionA::.ctor()
    IL_0022: throw
    IL_0023: pop

    // [8 5 - 8 29]
    IL_0024: call         !!0/*class [FSharp.Core]Microsoft.FSharp.Core.Unit*/ [assembly_A_optimized]assembly_A_optimized.referenceModulePublicA::makeExceptionNoInliningA<class [FSharp.Core]Microsoft.FSharp.Core.Unit>()
    IL_0029: pop

    // [10 5 - 10 19]
    IL_002a: call         !!0/*class [FSharp.Core]Microsoft.FSharp.Core.Unit*/ [assembly_B_not_optimized]assembly_B_not_optimized.referenceModulePublicB::makeExceptionB<class [FSharp.Core]Microsoft.FSharp.Core.Unit>()
    IL_002f: pop

    // [11 5 - 11 37]
    IL_0030: call         !!0/*class [FSharp.Core]Microsoft.FSharp.Core.Unit*/ [assembly_B_not_optimized]assembly_B_not_optimized.referenceModulePublicB::makeExceptionAggressiveInliningB<class [FSharp.Core]Microsoft.FSharp.Core.Unit>()
    IL_0035: pop

    // [12 5 - 12 29]
    IL_0036: call         !!0/*class [FSharp.Core]Microsoft.FSharp.Core.Unit*/ [assembly_B_not_optimized]assembly_B_not_optimized.referenceModulePublicB::makeExceptionNoInliningB<class [FSharp.Core]Microsoft.FSharp.Core.Unit>()
    IL_003b: pop

    // [14 5 - 14 6]
    IL_003c: ldc.i4.0
    IL_003d: ret

  } // end of method ProgramOptimized::main
} // end of class ProgramOptimized

and with optimization off for the using program, this compiles into:

// Location: C:\git\inlined_internal\usage_not_optimized\bin\Debug\netcoreapp3.1\usage_not_optimized.dll
// Sequence point data from C:\git\inlined_internal\usage_not_optimized\bin\Debug\netcoreapp3.1\usage_not_optimized.pdb

.class public abstract sealed auto ansi
  ProgramNotOptimized
    extends [System.Runtime]System.Object
{
  .custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAttribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags)
    = (01 00 07 00 00 00 00 00 ) // ........
    // int32(7) // 0x00000007

  .method public static int32
    main(
      string[] _arg1
    ) cil managed
  {
    .entrypoint
    .custom instance void [FSharp.Core]Microsoft.FSharp.Core.EntryPointAttribute::.ctor()
      = (01 00 00 00 )
    .maxstack 3
    .locals init (
      [0] string[] V_0
    )

    IL_0000: ldarg.0      // _arg1
    IL_0001: stloc.0      // V_0

    // [7 5 - 7 22]
    IL_0002: call         !!0/*class [FSharp.Core]Microsoft.FSharp.Core.Unit*/ [assembly_A_optimized]assembly_A_optimized.referenceModulePublicA::makeExceptionA<class [FSharp.Core]Microsoft.FSharp.Core.Unit>()
    IL_0007: pop

    // [8 5 - 8 40]
    IL_0008: call         !!0/*class [FSharp.Core]Microsoft.FSharp.Core.Unit*/ [assembly_A_optimized]assembly_A_optimized.referenceModulePublicA::makeExceptionAggressiveInliningA<class [FSharp.Core]Microsoft.FSharp.Core.Unit>()
    IL_000d: pop

    // [9 5 - 9 32]
    IL_000e: call         !!0/*class [FSharp.Core]Microsoft.FSharp.Core.Unit*/ [assembly_A_optimized]assembly_A_optimized.referenceModulePublicA::makeExceptionNoInliningA<class [FSharp.Core]Microsoft.FSharp.Core.Unit>()
    IL_0013: pop

    // [11 5 - 11 22]
    IL_0014: call         !!0/*class [FSharp.Core]Microsoft.FSharp.Core.Unit*/ [assembly_B_not_optimized]assembly_B_not_optimized.referenceModulePublicB::makeExceptionB<class [FSharp.Core]Microsoft.FSharp.Core.Unit>()
    IL_0019: pop

    // [12 5 - 12 40]
    IL_001a: call         !!0/*class [FSharp.Core]Microsoft.FSharp.Core.Unit*/ [assembly_B_not_optimized]assembly_B_not_optimized.referenceModulePublicB::makeExceptionAggressiveInliningB<class [FSharp.Core]Microsoft.FSharp.Core.Unit>()
    IL_001f: pop

    // [13 5 - 13 32]
    IL_0020: call         !!0/*class [FSharp.Core]Microsoft.FSharp.Core.Unit*/ [assembly_B_not_optimized]assembly_B_not_optimized.referenceModulePublicB::makeExceptionNoInliningB<class [FSharp.Core]Microsoft.FSharp.Core.Unit>()
    IL_0025: pop

    // [15 5 - 15 6]
    IL_0026: ldc.i4.0
    IL_0027: ret

  } // end of method ProgramNotOptimized::main
} // end of class ProgramNotOptimized

Thing to notice is that both the reference assembly and the 'using' program must be compiled with optimization on for this to occur. When the using program does not enable optimization all calls emit correct IL code. When the using program has optimization enabled only the function calls to the optimized reference assembly have inlined internal calls.

AlsoInternalsVisibleTo is not used in this example.

I hope this helps.