MXC - Microsoft's sandbox for code you shouldn't trust!

But you’re going to run anyway!

Every .NET developer knows this one, even if we don’t like to say it out loud. A NuGet package you pull in runs with the same permissions as your process. It gets your files, your network and your environment variables. We’ve lived with that for twenty years and call it “dependency management”.

Then AI agents showed up and we went one step further: we now let model output run commands on our machines. What could go wrong? A lot.

I’ve tried to solve this a couple of ways, with .NET libraries inside WebAssembly and with custom AssemblyLoadContext implementations. Both work, but neither is pleasant to adopt. You get different toolchains, extra build steps and a runtime boundary you have to design around. Sandboxing that’s hard to adopt doesn’t get adopted.

That’s why MXC (Microsoft eXecution Container) caught my eye. It’s open source (MIT), went GA on Windows 11 at the beginning of October 2026, and powers the local sandboxing in GitHub Copilot CLI. It takes the opposite approach: stay close to your code, with one SDK and a few lines of policy. It’s very new, so most of what follows comes from the project’s own documentation.

What it is

MXC is a library you build into your own application to run untrusted code (model output, plugins, tools) in a sandbox on Windows, Linux and macOS. It isn’t an agent platform, and installing it doesn’t make your machine safe. Your application decides what to run and under which rules. MXC enforces them.

The core is written in Rust, with Rust, .NET (Microsoft.Mxc.Sdk on NuGet) and Node SDKs on top. There’s also a raw executor (wxc-exec.exe) that takes JSON requests, for testing or for cases where you can’t embed the SDK. OpenAI Codex, GitHub Copilot, OpenClaw, Replit, LM Studio, NVIDIA OpenShell and Unsloth AI all support it, which makes sense when none of them wants to build a sandbox per OS.

The flow is simple. Your app builds a versioned request (command, container type, containment rules), MXC validates it and picks a backend, and the workload runs isolated from the rest of the machine. It’s the same idea as the Chromium sandbox: don’t trust the component that handles untrusted input, and limit what it can touch.

Backends: it uses what the OS gives you

Platform Default backend Alternatives
Windows 11 (x64 / ARM64) processcontainer windows_sandbox*, wslc, microvm*, hyperlight*, isolation_session
Linux (x64 / ARM64) bubblewrap lxc, microvm, hyperlight
macOS (ARM64 / x64) seatbelt none

* experimental

Isolation runs from an OS-level process sandbox up to a full VM, all behind one model. The guarantees are not identical across platforms. A process container is not a Hyper-V boundary, and the same policy on bubblewrap and on Windows (process container plus WFP for network, default-deny outbound) behaves differently. Check what you actually get on your target platform.

The policy is where security happens

Policy covers the filesystem (read-only, read-write, denied paths), the network (proxy and outbound controls) and the UI (clipboard, display, GUI). In C# it’s typed objects. This small demo starts from deny:

using Microsoft.Mxc.Sdk;
using Microsoft.Mxc.Sdk.V1;

var work = Directory.CreateTempSubdirectory("mxc-demo").FullName;
try
{
    File.WriteAllText(Path.Combine(work, "input.txt"), "hello from the host\n");

    var request = new ContainerRequest(
        "sh -c \"cat input.txt; echo x > blocked.txt && echo WRITE-OK || echo write-denied; " +
        "curl -s -m 3 https://example.com >/dev/null && echo NET-OK || echo net-denied\"")
    {
        Containment = new Containment.Process(),
        Filesystem = new FilesystemPolicy { ReadonlyPaths = { work } },
        Network = new NetworkPolicy
        {
            Egress = new NetworkEgressPolicy { Default = NetworkAction.Deny },
        },
        WorkingDirectory = work,
        TimeoutMs = 30_000,
    };

    try
    {
        var result = await MxcContainer.RunAsync(request);
        Console.Write(result.Stdout);
        Console.Error.Write(result.Stderr);
        foreach (var warning in result.Warnings)
        {
            Console.Error.WriteLine($"warning: {warning}");
        }
        Console.WriteLine($"exit={result.ExitCode} timedOut={result.TimedOut}");
    }
    catch (MxcException error)
    {
        Console.Error.WriteLine($"MXC error [{error.Code}]: {error.Message}");
        return 1;
    }
    return 0;
}
finally
{
    Directory.Delete(work, recursive: true);
}

The workload can read the one directory we hand it, can’t write to it, and can’t reach the network. The sh: line is stderr, which the code writes after stdout, so it appears last:

hello from the host
write-denied
net-denied
sh: 1: cannot create blocked.txt: Read-only file system
exit=0 timedOut=False

exit=0 is the shell’s exit code. The sandbox did its job, and the workload just didn’t get what it asked for. (The command is Linux-flavored. On Windows, use a cmd.exe equivalent.) A sandbox is only as strong as its policy. Give it read-write on your whole home directory and you’ve built a very elegant container with nothing inside it.

Containing parts of your app from .NET

MXC contains processes, not code inside your process. From .NET, your app orchestrates contained child workloads. You can’t fence off a single method.

Modern .NET has no in-process isolation (no CAS, no AppDomains), so you push the risky part out of process and contain that. Here are two patterns I’d use:

  • Self-relaunch worker. Start your own app via Environment.ProcessPath with a worker flag inside MXC and talk to it over stdio. Parsing untrusted files, deserializing third-party data and loading plugins then all happen contained. It’s the closest you get to “sandbox this method”.
  • Contained tool calls. With Agent Framework and/or Semantic Kernel, send every shell or code-exec tool call through MxcContainer.RunAsync with a per-tool policy instead of Process.Start. Each tool gets only the paths and hosts it needs.

What if my app does not work inside the sandbox? (Windows)

At first, it won’t, and the documentation says so. We’ve all seen the sandbox that was “temporarily” disabled in week two and stayed that way until the pentest.

On Windows (ProcessContainer) you can ask MXC to record every denied access:

var request = new ContainerRequest(command)
{
    Containment = new Containment.ProcessContainer
    {
        CaptureDenials = new CaptureDenialsPolicy(),
    },
    Filesystem = new FilesystemPolicy { ReadonlyPaths = { inputDir } },
    TimeoutMs = 30_000,
};

var result = await MxcContainer.RunAsync(request);

foreach (var warning in result.Warnings)
{
    Console.Error.WriteLine($"warning: {warning}");
}

if (result.OutputMetadata?.CaptureDenialsError is { } error)
{
    throw new InvalidOperationException(error.Message);
}

var capture = result.OutputMetadata?.CaptureDenials;
Console.WriteLine($"captured {capture?.TotalDenials} denial(s) in {capture?.OutputPath}");

By default (CaptureDenialsMode.Block) denials are recorded and enforced. You get a report of what to add to your policy, and the sandbox stays intact. That’s the mode to use in an application.

CaptureDenialsMode.Allow records accesses and lets them through, so nothing is enforced. Use it only to learn what your own trusted tool needs, and never with untrusted code. Then write the policy, switch back to Block and test again. Check BackendCapability.CaptureDenials to see whether the current host supports it (Windows only).

This could also work as a feedback loop for Interactive Application Security Testing (IAST). Run an instrumented application in Allow mode on a test machine and see which files it touches and which external addresses it reaches out to.

What I would check before using it

  • Threat model first. What is the untrusted input, and what’s the worst the workload can do with the rights you give it?
  • Stay on the default backend unless you know why you need another. The experimental ones are marked experimental for a reason.
  • Test the denials. Try to read a denied path and reach a blocked host, and make sure both fail.
  • Expect uneven visibility. Only Windows process containers produce an agent activity report. Session containers, WSLc and microVMs don’t.
  • Telemetry is off unless the run opts in, the user consents and admin policy allows it. Local open-source builds send nothing to Microsoft. Verify that in your own build.
  • It’s one layer. A sandbox reduces blast radius, but it doesn’t make bad code good. You still need least privilege, review and monitoring.

Conclusion

I’m skeptical by profession, and “new security sandbox” usually sends me looking for the part where it breaks. Still, the approach is sound: deny by default, policy as data, native OS isolation per platform, and honest documentation about the limits. If you’re building agents or plugin systems, it’s a good first step. It still needs least privilege, review and monitoring around it.

Documentation and source: https://github.com/microsoft/mxc

Further reading:

C# Source Generators

A week ago Microsoft released a preview of a new compiler feature called C# Source Generators. This feature allows to inspect existing source-code and generate additional C# source files that will be added to the compiled output. One of the goals is to use this in order to remove the need for any (slower) dynamic runtime behavior that rely on reflection. There are also benefits from security perspective that can be added to that list, let’s look into that a bit further!

Source Generator

When a Source Generator is added to a .NET Core project it will be executed when the project is being build. It has the ability to inspect the programs source artifacts and create additional files that will be added to the output. The concept is also called Compile Time Metaprogramming as explained in the design document. Source Generators have a strong relationship with existing Roslyn Analyzers that allow you to inspect both syntax and the semantic models of the compilation.

Application Parts in ASP.NET Core MVC

When you create an ASP.NET Core MVC application and add new functionality with either implementations of Controllers and Views or Razor Pages it all just works when you start it. One key element in making all of this possible is a component called the ApplicationPartsManager. This manager will contain a list of FeatureProviders that will dynamically populate the needed content for e.g. Razor Pages, Controllers, TagHelpers and Views. Read the Share controllers, views, Razor Pages and more with Application Parts article in the Microsoft documentation for more details on how it works.

What is exposed by ASP.NET Core MVC?

The whole way of automagically resolving the content can be really helpful when developing. One big downside is the fact that there is a risk of exposing more than intended. From the main MVC project (the one that constructs the host builder), any referenced project or added package that also reference the Microsoft.AspNetCore.Mvc package will automatically be scanned for controllers. The class needs to match a naming convention that ends with ...Controller (including inheritance) or is annotated with the Microsoft.AspNetCore.Mvc.ControllerAttribute. Each identified controller will then have all it’s public methods exposed and keep in mind that any used third-party package has the ability to do exactly the same. It is not unseen that packages get compromised (there are a lot of ways!) by people who have got other and/or bad intentions. Of course it’s possible to determine a list of exposed controllers at runtime but I think it’s better to have more control (e.g. whitelist of allowed ones) from the start, and earliest possible will of course be at compile time.

Determine exposed controllers at compile time

A bit more than a year ago David Fowler shared a tweet in which he explained that one of the design goals of ASP.NET Core was to remove the scanning of assemblies at start up to find things that are exposed. And that is exactly what the new compiler feature of C# Source Generators will bring to ASP.NET Core MVC. Currently the samples show some basic scenario’s on how it can be used, I’m really looking forward on seeing an implementation of it in the ASP.NET Core MVC packages. An additional benefit of all of this is that from static analysis perspective there will be less need to understand what happens at runtime when resolving controllers dynamically, because it’s all just part of the compiled assembly.

Hello Blog!

Welcome to the first post on my blog, with definitely a lot more to follow! I’ve been thinking about doing this for a while and realized that this is the right moment to start one. Over the last years I’ve done a lot of speaking on various topics and with this blog I’m going to take it a bit further with sharing more content more frequently.

What will I write about? Because I work as a security researcher for Veracode and as the subtitle of the blog indicates, it will mostly be about application/software security and .NET. But definitely won’t mean it will be exclusively about that. Lately I’ve been playing around with Rust and really enjoying it. Same counts for all things happening around WebAssembly, which of course is not only limited to Blazor. Want to know more about me and my background? I suggest to read the About Me page for that.

Hope to see you back soon! You can always start following me on Twitter @nielstanis where I’ll also share if there is new content available. And on the page about Speaking you can find links to the content of the talks I’ve given in the past.