2

I have a multi-tenant .NET 5 application, running in ASP.NET, in which a request can run arbitrary code and use an arbitrary amount of memory. How can I protect against users using too much memory, on a per-request basis?

I'm aware that I can limit per-process, and also that I can limit the size of a HTTP request being made, but that's not what I need. I need a way that if a user runs code like int array = int[1,000,000,000,000,000,000,000] they will get an exception rather than the entire site coming down.

Edit: this is a jsfiddle style application (specifically, it's darklang.com), though with a custom language and runtime, so I have pretty good control over the runtime.

Paul Biggar
  • 27,579
  • 21
  • 99
  • 152
  • That seems like an architectural design decision that you would make up front, rather than go-live!. How about running the code in it's own app domain? https://stackoverflow.com/questions/1094478/what-is-a-net-application-domain . Apparently App Domains are no longer a thing, use LoadContexts instead? – Mitch Wheat Apr 04 '21 at 04:15
  • ...or perhaps not: https://learn.microsoft.com/en-us/dotnet/api/system.appdomain?view=net-5.0 : "Use application domains to isolate tasks that might bring down a process. If the state of the AppDomain that's executing a task becomes unstable, the AppDomain can be unloaded without affecting the process. This is important when a process must run for long periods without restarting" – Mitch Wheat Apr 04 '21 at 04:21
  • I don't know how you can limit memory but maybe throttling number of request could help. You could use [AspNetCoreRateLimit](https://github.com/stefanprodan/AspNetCoreRateLimit) or it's not core twin. – tymtam Apr 04 '21 at 04:39
  • 1
    " if a user runs code " huh? aren't you talking about developers writing code? Why would an end user of a web-app be writing code? What kind of code? Take a step back and describe your actual problem. – Jeremy Lakeman Apr 07 '21 at 02:05
  • 1
    I am assuming you are trying to build an API that allows people to write code in the browser, submits it to the backend and at that point, you'd compile and run it dynamically. Like those [code websites](https://onecompiler.com/csharp) out there. – Andy Apr 07 '21 at 02:41
  • 1
    @JeremyLakeman: Consider a website like [dotnetfiddle](https://dotnetfiddle.net) as an example of how a user can "run code on the backend". – Flater Apr 07 '21 at 11:09
  • @jeremylakeman As Andy and Flater guessed, it's similar to a dotnetfiddle situation. It's our own custom language and runtime so we have flexibility for how we do this (eg if there was options to use a custom allocator or similar, that could work) – Paul Biggar Apr 07 '21 at 12:43
  • Can you cancel a running request at all? You can tell its resource requirement only after it has started using too much memory. If you have a way to cancel a request you can abort the last started call and hope that this was the bad one if not continue to cancel all other previously started requests. An easy way out would be to start each request in an extra process then you have complete control. – Alois Kraus Apr 07 '21 at 13:43
  • @AloisKraus I believe i can use a cancellabletoken to abort a running request. I would need to be able to measure it in that case, which I'm not sure how to do. – Paul Biggar Apr 07 '21 at 15:51
  • @PaulBiggar: A CancellationToken wont help if you execute arbitrary code. The token only supports cooperative cancellation where you need to check if cancellation was requested. If you want complete control you can spawn a bunch of n-worker processes which are reused between requests. If any worker process grows too large you can simply kill it. – Alois Kraus Apr 07 '21 at 16:50
  • @AloisKraus The code is arbitrary but the language is implemented by me and running under my control. It uses tasks throughout, so I think I wouldn't have any problem with adding CTS support – Paul Biggar Apr 07 '21 at 17:11
  • Running someone else's code I would surely do in another process. Maybe even spin up a docker instance for the task. – Jesse de Wit Apr 08 '21 at 12:11
  • @PaulBiggar is the arbitrary code allocating managed memory or unmanaged? – Kit Apr 08 '21 at 15:28
  • @PaulBiggar is the amount of memory the arbitrary code is going to allocate knowable beforehand in any way? If so how? – Kit Apr 08 '21 at 15:32
  • @kit The code is run in an interpreter written in F#, connected to Kestrel/ASP.NET. I wrote the interpreter and can change it as needed. So I guess it's all managed. The amount of memory is not knowable. – Paul Biggar Apr 08 '21 at 16:35

2 Answers2

1

.NET CORE and .NET 5

Because the question is about .Net Core I must include why going AppDomain route won't work.

App Domains

Why was it discontinued? AppDomains require runtime support and are generally quite expensive. While still implemented by CoreCLR, it’s not available in .NET Native and we don’t plan on adding this capability there.

What should I use instead? AppDomains were used for different purposes. For code isolation, we recommend processes and/or containers. For dynamic loading of assemblies, we recommend the new AssemblyLoadContext class.

Source: Porting to .NET Core | .NET Blog

This leaves us only one way to do this if you want to have "automatic" information about memory usage.

How to measure memory usage for a separate process

To measure other process memory we can use the Process handle and get its WorkingSet, PrivateMemory, and VirtualMemory. More about memory types

The code to handle another process is quite simple.

private Process InterpreterProcess;

// Run every how often you want to check for memory
private void Update()
{
    var workingSet = InterpreterProcess.WorkingSet64;
    if(workingSet > Settings.MemoryAllowed)
    {
        InterpreterProcess.Kill(true);
    }
}

private void Start()
{
    InterpreterProcess = new Process(...);
    // capture standard output and pass it along to user
    Task.Run(() =>
    {
        Update();
        Thread.Sleep(50);
        // This will be also convenient place to kill process if we exceeded allowed time
    });
}

This, however, leaves us with a very important question, since we might allow users to access system critical resources - even if we do not run Administrator privileges on the Process.

Alternative approach

Since you mentioned that you have a custom interpreter it might be easier for you to add memory management, memory counting, and security to the interpreter.

Since we can assume that memory allocation is only made with new your interpreter needs to just test the size of each new allocation and test accordingly.

To test managed object size you need to "analyze" created instance and use sizeof() on each simple type, a proper way is in the making There's no way to get the managed object size in memory #24200

Tomasz Juszczak
  • 2,276
  • 15
  • 25
  • "Since we can assume that memory allocation is only made with new your interpreter needs to just test the size of each new allocation and test accordingly." -> Alas, it's in F#, so there's no `new`s here – Paul Biggar Apr 11 '21 at 20:43
  • So you are left only with separate Process approach or wait for proper implementation form dotnet team to get memory size of an object – Tomasz Juszczak Apr 12 '21 at 20:17
1

You need to use a process (within a job object or cgroup), there's no way to do this at the application level in .NET.

davidfowl
  • 37,120
  • 7
  • 93
  • 103