6

Is there a way to limit the maximum memory a C# method is allowed to allocate? We are processing complex user-provided input and we realized that it is possible to DoS our service by providing a certain special input. We are not able to detect all variations of DoS attacks, so we want to limit processing time and memory allocation of our ProcessInput() method.

Processing time is "easy", we just run a timer and cancel the ProcessInput action via a CancellationToken. However, we haven't found a (simple) solution for memory allocation yet. We do not want to write our own memory manager and use an object array or something like that.

D.R.
  • 20,268
  • 21
  • 102
  • 205
  • 1
    Maybe you can start another `AppDomain` and watch the memory from there: https://stackoverflow.com/q/27110662/993547. – Patrick Hofman Sep 28 '17 at 07:52
  • You should probably use a separate process. – Andrei Tătar Sep 28 '17 at 07:54
  • 1
    Maybe some use of [`GC.RegisterForFullGCNotification`](https://msdn.microsoft.com/en-us/library/system.gc.registerforfullgcnotification%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396) would help? – Matthew Watson Sep 28 '17 at 07:56
  • Do you need this for only one method, or can you use the application's start and current memory usage status? – Taha Paksu Sep 28 '17 at 08:00
  • 2
    Memory is a process-wide resource, you cannot limit its usage to a method or thread or AppDomain. The only thing that AppDomain gives you is the ability to recover from an OOM crash, but you can't predict what code is going to raise that exception so is pretty useless. Nor can you assume that you will always be able to stop it in time, there is no guarantee that such monitor code would run timely enough nor that the untrusted code will stop timely enough. You'll have to isolate untrusted code like this in its own process. – Hans Passant Sep 28 '17 at 08:28
  • @HansPassant: We do not want to run into OOM exceptions, we want to limit the memory at a much much lower point. A best-effort approach where we limit the memory to "around x MB" is also a good-enough solution for us. Do you think the AppDomain approach could work with this setting? – D.R. Sep 28 '17 at 08:32
  • 2
    If you know it doesn't crash then AppDomain doesn't buy you anything useful. You'll need another guarantee that GC.GetTotalMemory() only tells you about memory allocated by that code. Don't use any threads and you can assume that. That's fairly chicken-and-egg. Consider that memory usage tends to be proportional to time for code like that. A watch dog timer is much easier to implement. – Hans Passant Sep 28 '17 at 08:44

1 Answers1

2

GC heap(s) are per process, not per AppDomain - so for correct estimates you'd need to spawn a separate process.

You can even host CLR yourself to influence segment sizes and get notifications. However if nothing else is running in the process and you're OK with estimate then GC.GetTotalMemory(). This would however need to be executed in 'NoGC' region if you are interrested in 'total ever consumed memory during the method run' as opposed to 'maximum total memory being used at any point of time' - as GC can trigger several times during your method run.

To limit perf/resources impact of spawning processes, you can spawn N processes - where N is your desired concurrency level - and than have each process pull tasks from work-stealing queue in central process, while the subprocesses process request synchronously.

A dirty idea how it can look like (you'd need to handle results reporting plus 100 other 'minor' things):

Main process:

    public void EnqueueWork(WorkRequest request)
    {
        _workQueue.Enqueue(request);
    }

    ConcurrentQueue<WorkRequest> _workQueue = new ConcurrentQueue<WorkRequest>();

    [OperationContract]
    public bool GetWork(out WorkRequest work)
    {
        return _workQueue.TryDequeue(out work);
    }

Worker processes:

    public void ProcessRequests()
    {
        WorkRequest work;
        if (GetWork(out work))
        {
            try
            {
                //this actually preallocates 2 * _MAX_MEMORY - for ephemeral segment and LOH
                // it also performs full GC collect if needed - so you don't need to call it yourself
                if (!GC.TryStartNoGCRegion(_MAX_MEMORY))
                {
                    //fail
                }

                CancellationTokenSource cts = new CancellationTokenSource(_MAX_PROCESSING_SPAN);
                long initialMemory = GC.GetTotalMemory(false);

                Task memoryWatchDog = Task.Factory.StartNew(() =>
                {
                    while (!cts.Token.WaitHandle.WaitOne(_MEMORY_CHECK_INTERVAL))
                    {
                        if (GC.GetTotalMemory(false) - initialMemory > _MAX_MEMORY)
                        {
                            cts.Cancel();
                            //and error out?
                        }
                    }
                })

                DoProcessWork(work, cts);

                cts.Cancel();

                GC.EndNoGCRegion();
            }
            catch (Exception e)
            {
                //request failed
            }
        }
        else
        {
            //Wait on signal from main process
        }
    }
Jan
  • 1,905
  • 17
  • 41