0

I'm running code in a locked-down AppDomain sandbox. Exceptions thrown from within this AppDomain don't include line numbers, even though pdbs are available. The code that tries to access the stack trace is fully trusted: assembly is signed and loaded as strong assembly into the app domain. So I would've expected the stack trace to contain file names and line numbers. I cannot mark the AppDomain as fully trusted as that would defeat the purpose of the sandbox. How can I have my stack traces contain file names and line numbers?

Update I have updated the code to show how the external code is loaded using Assembly.LoadFile. My original question used a single assembly, which appeared to me to show the same behaviour as in my 'real' application. However as the answer from @simon-mourier worked for this simplified code, it doesn't work in my 'real' application. I have updated the code to reflect this.

The following sample code (based on this example) shows the problem. There are two assemblies:

  • the executing (parent) assembly which is signed and has full trust
  • the child assembly which is not signed and must not be trusted.

I've posted the example application on my github.

// Executing signed assembly
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Permissions;
using System.Security.Policy;

namespace Parent
{
    public class Worker : MarshalByRefObject
    {
        private static string childpath = Path.Combine(Path.GetDirectoryName(typeof(Worker).Assembly.Location), "Child.dll");

        private static void Main()
        {
            var w = new Worker();
            w.TestExceptionStacktrace();

            var adSandbox = GetInternetSandbox();
            var handle = Activator.CreateInstanceFrom(
                adSandbox,
                typeof(Worker).Assembly.ManifestModule.FullyQualifiedName,
                typeof(Worker).FullName);
            w = (Worker)handle.Unwrap();
            w.TestExceptionStacktrace();
        }

        public void TestExceptionStacktrace()
        {
            TestInner();
        }

        private void TestInner()
        {
            var ass = Assembly.LoadFile(childpath);

            var playMethod = ass.GetTypes()[0].GetMethod("Play");

            try
            {
                playMethod.Invoke(null, Array.Empty<object>());
            }
            catch (Exception e)
            {
                var s = e.ToString();
                Console.WriteLine("Stack trace contains {0}line numbers for Child assembly:",
                    s.Split(new[] { Environment.NewLine }, StringSplitOptions.None)
                        .Single(x => x.Contains("Play()")).Contains("line")
                        ? ""
                        : "no ");
                Console.WriteLine($"   {s}");
            }
        }

        // ------------ Helper method ---------------------------------------
        private static AppDomain GetInternetSandbox()
        {
            // Create the permission set to grant to all assemblies.
            var hostEvidence = new Evidence();
            hostEvidence.AddHostEvidence(new Zone(
                System.Security.SecurityZone.Internet));
            var pset =
                System.Security.SecurityManager.GetStandardSandbox(hostEvidence);

            // add this to the permission set
            pset.AddPermission(new FileIOPermission(
                FileIOPermissionAccess.PathDiscovery, typeof(Worker).Assembly.Location)
            );
            pset.AddPermission(new FileIOPermission(
                FileIOPermissionAccess.PathDiscovery | FileIOPermissionAccess.Read, Path.GetDirectoryName(childpath))
            );

            // Identify the folder to use for the sandbox.
            var ads = new AppDomainSetup();
            ads.ApplicationBase = System.IO.Directory.GetCurrentDirectory();

            var fullTrustAssemblies = new[]
            {
                typeof(Worker).Assembly.Evidence.GetHostEvidence<StrongName>(),
            };

            // Create the sandboxed application domain.
            return AppDomain.CreateDomain("Sandbox", hostEvidence, ads, pset, fullTrustAssemblies);
        }
    }
}
// Child assembly (not signed)
using System;

namespace Child
{
    public class Child
    {
        public static void Play()
        {
            var ad = AppDomain.CurrentDomain;
            Console.WriteLine("\r\nApplication domain '{0}': IsFullyTrusted = {1}",
                ad.FriendlyName, ad.IsFullyTrusted);

            Console.WriteLine("   IsFullyTrusted = {0} for the current assembly {1}",
                typeof(Child).Assembly.IsFullyTrusted,
                typeof(Child).Assembly);

            Console.WriteLine("   IsFullyTrusted = {0} for mscorlib",
                typeof(int).Assembly.IsFullyTrusted);

            throw new Exception("Some exception");
        }
    }
}

The output of this code is:

Application domain 'Parent.exe': IsFullyTrusted = True
   IsFullyTrusted = True for the current assembly Child, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
   IsFullyTrusted = True for mscorlib
Stack trace contains line numbers for Child assembly:
   System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.Exception: Some exception
   at Child.Child.Play() in C:\Users\Bouke\Developer\SandboxStacktrace\Child\Child.cs:line 20
   (...)

Application domain 'Sandbox': IsFullyTrusted = False
   IsFullyTrusted = False for the current assembly Child, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
   IsFullyTrusted = True for mscorlib
Stack trace contains no line numbers for Child assembly:
   System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.Exception: Some exception
   at Child.Child.Play()
   (...)

I've so far discovered that using Assembly.Load(byte[], byte[], SecurityContextSource.CurrentAssembly) works, but defeats the sandbox (loading assembly has full trust). Assembly.Load(byte[], byte[], SecurityContextSource.CurrentAppDomain) doesn't work.

Bouke
  • 11,768
  • 7
  • 68
  • 102

1 Answers1

0

The Internet zone is quite restrictive. An AppDomain that needs stack traces needs some access to the file itself, so you need to add some file permission to the permission set you use when you create the AppDomain, something like this:

...

// add this to the permission set
pset.AddPermission(new FileIOPermission(
    FileIOPermissionAccess.PathDiscovery, typeof(Worker).Assembly.Location)
    );

return AppDomain.CreateDomain("Sandbox", hostEvidence, ads, pset, fullTrustAssemblies);

Note: when you have problems with CAS, you can use the AppDomain's FirstChanceException event (or enable Visual Studio's all exceptions if you use it) that will show you errors that are swallowed internally.

Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • This sadly demonstrates that the repro code I provided was simply insufficient to show the actual problem. While your change works for the original posted code, it doesn't help me with my actual problem which uses `Assembly.LoadFile(string)` to load external untrusted code. I don't quite understand why that makes a difference, but here we are. I've updated my question to reflect this. Thank you anyways for your time. – Bouke Mar 08 '22 at 10:09
  • If you add a FirstChanceException event as I said, you will see that you need to add `ReflectionPermission(PermissionState.Unrestricted)` to the pset. Then, you will see that you need to add `SecurityPermission(SecurityPermissionFlag.ControlEvidence | SecurityPermissionFlag.ControlPolicy)` to the pset. Then, you will see that the system will tell you that "The Zone of the assembly that failed was: MyComputer" which basically means it cannot do much with it IMHO. PS: replace `Single` by `SingleOrDefault` in your catch code otherwise it creates additional issues. – Simon Mourier Mar 08 '22 at 10:47
  • I've added `FirstChanceException` to the github repo. I needed to add `ReflectionPermission(PermissionState.Unrestricted)`. Where do you see the need for `SecurityPermission(SecurityPermissionFlag.ControlEvidence | SecurityPermissionFlag.ControlPolicy)`, as I don't. There is another exception logged by FCE: "Stack walk modifier must be reverted before another modification of the same type can be performed." which should be [unrelated](https://stackoverflow.com/questions/69726316/bug-in-net-framework-mscorlib-preventing-stack-trace-line-numbers-in-portabl#comment123249159_69726316). – Bouke Mar 08 '22 at 12:48
  • They are inner exceptions to SecurityPermission. You don't see them because you're only dumping type & message. Note this will cause stackoverflow, but you still can see the demand. – Simon Mourier Mar 08 '22 at 12:59
  • I get two unexpected FCE's. They are both `SecurityException`s regarding that stack walk modifier. They don't have a demand nor an inner exception. From what I understand from the previously linked question is that these exceptions are caused by the reader of "portable pdbs", but I'm not using portable pdbs. What other exception could you be referring to? – Bouke Mar 08 '22 at 13:17
  • I have now disabled support for portable pdbs (app.config). That gets rid of the remaining `SecurityException`s seen by FCE. There are now only two expected FCE's remaining: the one I throw and the wrapping exception. – Bouke Mar 08 '22 at 13:53
  • Dump the exception ToString() and you should see: https://i.imgur.com/rJoEytM.png – Simon Mourier Mar 08 '22 at 15:37
  • My bad, I didn't include `App.config` in the `csproj`, so it didn't take effect. If you add it to the project (see update on GH), "portable pdb" support is disabled and you'll that exception is no longer thrown. -- Still no debug symbols though :(. – Bouke Mar 08 '22 at 16:04