Подтвердить что ты не робот

Держите TypedReference живым из блока метода, не возвращая его

Я хочу предположить, что цель этого вопроса - проверить, существует ли хотя бы один способ, даже если с помощью самого небезопасного взлома, сохранить ссылку на тип не-blittable value. Я знаю, что такой тип дизайна сопоставим с преступлением; Я бы не использовал его в каком-либо практическом случае, кроме обучения. Поэтому, пожалуйста, примите, пожалуйста, читать еретичный небезопасный код.

Мы знаем, что ссылку на blittable-тип можно сохранить и увеличить следующим образом:

unsafe class Foo
{
    void* _ptr;

    public void Fix(ref int value)
    {
        fixed (void* ptr = &value) _ptr = ptr;
    }

    public void Increment()
    {
        var pointer = (int*) _ptr;
        (*pointer)++;
    }
}

С точки зрения безопасности, вышеупомянутый класс сопоставим с прыжком в пустоте (каламбур не предназначен), но он работает, как уже упоминалось здесь. Если переменная, выделенная в стеке, передается ему, а затем область метода вызывающего абонента завершается, вы, вероятно, столкнетесь с ошибкой или явной ошибкой нарушения доступа. Однако, если вы выполните такую ​​программу:

static class Program
{
    static int _fieldValue = 42;

    public static void Main(string[] args)
    {
        var foo = new Foo();
        foo.Fix(ref _fieldValue);
        foo.Increment();
    }
}

Класс не будет размещен до тех пор, пока не будет выгружен относительный домен приложения, и поэтому применим для этого поля. Я честно не знаю, можно ли перераспределить поля в высокочастотной куче, но лично я не думаю. Но пусть откладывает безопасность еще больше (если возможно). Прочитав этот и этот вопрос, мне было интересно, есть ли способ создать подобный подход для не- и поэтому я сделал эту программу, которая на самом деле работает. Прочитайте комментарии, чтобы узнать, что он делает.

static class Program
{
    static Action _event;

    public static void Main(string[] args)
    {
        MakerefTest(ref _event);
        //The invocation list is empty again
        var isEmpty = _event == null;
    }

    static void MakerefTest(ref Action multicast)
    {
        Action handler = () => Console.WriteLine("Hello world.");
        //Assigning a handler to the delegate
        multicast += handler;
        //Executing the delegate invocation list successfully
        if (multicast != null) multicast();
        //Encapsulating the reference in a TypedReference
        var tr = __makeref(multicast);
        //Removing the handler
        __refvalue(tr, Action) -= handler;
    }
}

Актуальная проблема/возможность:

Мы знаем, что компилятор не позволит нам сохранить значение, переданное с помощью ссылки ref, но ключевое слово __makeref, как и недокументированное и недооцененное, дает возможность инкапсулировать и восстановить ссылку на типы, способные к блужданию. Однако возвращаемое значение __makeref, TypedReference хорошо защищено. Вы не можете сохранить его в поле, вы не можете его вставить, вы не можете создать его массив, вы не можете использовать его в анонимных методах или лямбда. Все, что мне удалось сделать, это изменить приведенный выше код следующим образом:

static void* _ptr;

static void MakerefTest(ref Action multicast)
{
    Action handler = () => Console.WriteLine("Hello world.");
    multicast += handler;
    if (multicast != null) multicast();
    var tr = __makeref(multicast);
    //Storing the address of the TypedReference (which is on the stack!)
    //inside of _ptr;
    _ptr = (void*) &tr;
    //Getting the TypedReference back from the pointer:
    var restoredTr = *(TypedReference*) _ptr;
    __refvalue(restoredTr, Action) -= handler;
}

Вышеприведенный код работает так же хорошо и выглядит еще хуже, чем раньше, но ради знания я хотел сделать больше с ним, поэтому написал следующее:

unsafe class Horror
{
    void* _ptr;

    static void Handler()
    {
        Console.WriteLine("Hello world.");
    }

    public void Fix(ref Action action)
    {
        action += Handler;
        var tr = __makeref(action);
        _ptr = (void*) &tr;
    }

    public void Clear()
    {
        var tr = *(TypedReference*) _ptr;
        __refvalue(tr, Action) -= Handler;
    }
}

Класс Horror представляет собой комбинацию класса Foo и указанного выше метода, но, как вы, несомненно, заметите, у него есть одна большая проблема. В методе Fix объявлен TypedReference tr, его адрес копируется внутри общего указателя _ptr, тогда метод заканчивается и tr больше не существует. Когда вызывается метод Clear, "новый" tr поврежден, потому что _ptr указывает на область стека, которая больше не является TypedReference. Итак, возникает вопрос:

Есть ли способ обмануть компилятор в сохранении экземпляра TypedReference в течение неопределенного промежутка времени?

Любой способ добиться желаемого результата будет считаться хорошим, даже если это связано с уродливым, небезопасным, медленным кодом. Класс, реализующий следующий интерфейс, был бы идеальным:

interface IRefStorage<T> : IDisposable
{
    void Store(ref T value);
    //IDisposable.Dispose should release the reference
}

Пожалуйста, не обсуждайте вопрос как общее обсуждение, потому что его цель состоит в том, чтобы увидеть, есть ли способ хранения ссылок на типы, которые могут быть сбиты с толку, такими же злыми, как это может быть.

Последнее замечание, я знаю о возможностях связывания полей через FieldInfo, но мне показалось, что последний метод не очень сильно поддерживает типы, происходящие из Delegate.

Возможное решение (результат баунти)

Я бы назвал AbdElRaheim ответом, как только он отредактировал свой пост, чтобы включить решение, которое он представил в своем комментарии, но я полагаю, что это было не очень ясно. В любом случае, среди методов, которые он предоставил, тот, который был подведен в следующем классе (который я слегка изменил), казался самым "надежным" (иронично использовать этот термин, поскольку мы говорим об использовании взлома):

unsafe class Horror : IDisposable
{
    void* _ptr;

    static void Handler()
    {
        Console.WriteLine("Hello world.");
    }

    public void Fix(ref Action action)
    {
        action += Handler;
        TypedReference tr = __makeref(action);
        var mem = Marshal.AllocHGlobal(sizeof (TypedReference)); //magic
        var refPtr = (TypedReference*) mem.ToPointer();
        _ptr = refPtr;
        *refPtr = tr;
    }

    public void Dispose()
    {
        var tr = *(TypedReference*)_ptr;
        __refvalue(tr, Action) -= Handler;
        Marshal.FreeHGlobal((IntPtr)_ptr);
    }
}

Что Fix делает, начиная с строки, помеченной как "magic" в комментарии:

  • Выделяет память в процессе - в неуправляемой части.
  • Объявляет refPtr как указатель на TypedReference и устанавливает его значение указателю области памяти, выделенной выше. Это делается вместо прямого использования _ptr, потому что поле с типом TypedReference* генерирует исключение.
  • Неявно переводит refPtr в void* и присваивает указатель _ptr.
  • Устанавливает tr как значение, указанное refPtr и, следовательно, _ptr.

Он также предложил другое решение, которое он первоначально написал как ответ, но он казался менее надежным, чем тот, который был выше. С другой стороны, было также другое решение, предоставленное Питером Уишартом, но для этого требовалась точная синхронизация, и каждый экземпляр Horror "потерял" поток. Я воспользуюсь случаем, чтобы повторить, что вышеупомянутый подход находится в нет, предназначенном для использования в реальном мире, это был просто академический вопрос. Я надеюсь, что это будет полезно для тех, кто читает этот вопрос.

4b9b3361

Ответ 1

Чем вы пытаетесь это сделать? Локали находятся в стеке, и аргументы тоже зависят от соглашения о вызовах. Хранение или возврат адреса локального или аргумента не является хорошим, потому что он будет перегружен. Невозможно предотвратить их переоценку, кроме как не вызывать методы.

Если вы включите неуправляемую отладку, вы можете использовать отладчик памяти и зарегистрировать окно, чтобы узнать, что происходит.

Здесь проще понять пример C. Почему печать не отображает правильное значение. Поскольку при вызове функции печати его стек стека перезаписывает значение.

int* bad(int x, int y)
{
    int sum = x + y;
    return &sum;
};

int* bad2(int x, int y)
{
    x += y;
    return &x;
}

int _tmain(int argc, _TCHAR* argv[])
{
    int* sum1 = bad(10, 10);
    int* sum2 = bad(100, 100);
    printf("%d bad", *sum1);  // prints 200 instead of 20

    sum1 = bad2(10, 10);
    sum2 = bad2(100, 100);
    printf("%d bad", *sum1);  // prints 200 instead of 20

    return 0;
};

Не могу заставить clr держаться за него дольше. Одна вещь, которую вы можете сделать, это заставить переменную в стеке продвинуться дальше. Ниже приведен пример. Это все плохо: (

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Xml.Linq;
using System.Runtime.InteropServices;

namespace Bad
{
    class Program
    {
        static void Main(string[] args)
        {
            Action a = () => Console.WriteLine("test");
            Horror h = new Horror();
            h.Fix(new Big(), ref a, new Big());
            h.Clear();
            Console.WriteLine();
        }
    }
    [StructLayout(LayoutKind.Sequential, Size = 4096)]
    struct Big
    {
    }
    unsafe class Horror
    {
        void* _ptr;

        static void Handler()
        {
            Console.WriteLine("Hello world.");
        }


        public void Fix(Big big, ref Action action, Big big2)
        {
            action += Handler;
            var tr = __makeref(action);
            _ptr = (void*)&tr;
        }

        public void Clear()
        {
            var tr = *(TypedReference*)_ptr;
            __refvalue(tr, Action) -= Handler;
        }
    }
}

Ответ 2

Да! Вы можете обмануть компилятор в создании поля типа TypedReference, создав итератор или лямбда, который использует один (или, возможно, блок async):

static IEnumerable<object> Sneaky(TypedReference t1)
{
    yield return TypedReference.ToObject(t1);
}

static Func<object> Lambda(TypedReference t1)
{
    return () => TypedReference.ToObject(t1);
}

К сожалению, CLR не позволяет вам фактически использовать класс. При попытке вы получите TypeLoadException.

Другими словами, типы TypedReference и ArgIterator защищены не только компилятором, но и временем выполнения. Если вы хотите сохранить копию одного из них, вам нужно будет сделать это, сделав небезопасное blit на кучу или отражение.

Обратите внимание, что я пробовал это с компилятором .NET 4.0 С#. Другие компиляторы могут быть более умными.

Ответ 3

Вы также можете добиться этого без использования неуправляемой памяти, создав "поддельный" тип, который напоминает типизированную ссылку в своей структуре:

unsafe class Horror
{
    FakeTypedRef ftr;

    static void Handler()
    {
        Console.WriteLine("Hello void.");
    }

    public void Fix(ref Action action)
    {
        action += Handler;
        TypedReference tr = __makeref(action);
        ftr = *(FakeTypedRef*)(&tr);
    }

    public void Clear()
    {
        fixed(FakeTypedRef* ptr = &ftr)
        {
            var tr = *(TypedReference*)ptr;
            __refvalue(tr, Action) -= Handler;
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    struct FakeTypedRef
    {
        public IntPtr Value;
        public IntPtr Type;
    }
}

Важное изменение: Я настоятельно рекомендую против передавать любую ссылку в качестве указателя. GC разрешено свободно перемещать объекты на управляемой куче по мере ее соответствия, и нет никакой гарантии, что указатель останется действительным даже после того, как он будет возвращен из метода. Вы можете не видеть непосредственный эффект этого из-за отладки, но вы разыгрываете всевозможные проблемы.

Если вам действительно нужно обработать его как указатель (и могут быть некоторые законные причины), вам нужно исправить пользовательский CIL с закрепленной ссылкой. Его можно даже инициализировать, извлекая указатель из TypedReference, но он гарантирует, что местоположение не изменится. Затем передайте его методу лямбда.

Ответ 4

TypedReference кажется довольно окончательно заблокированным.

Я думаю, проще было просто заблокировать этот тип, чтобы он был безопасным, а не позволял его передавать, но только в небезопасном контексте.

Вы можете держаться за одного, пока... будет стоить вам нить, хотя:)

namespace TehHorror
{
    using System;
    using System.Threading;    
    class Horror
    {
        private ManualResetEvent waiter = null;    
        public void Fix(ref Action multicast)
        {
            waiter = new ManualResetEvent(false);
            multicast += HorrorHandler;
            if (multicast != null) multicast();
            var tr = __makeref(multicast);
            waiter.WaitOne();
            __refvalue(tr, Action) -= HorrorHandler;
        }    
        public void Clear() { waiter.Set(); }    
        private static void HorrorHandler()
        {
            Console.WriteLine("Hello from horror handler.");
        }
    }    
    class Program
    {
        static void Main()
        {
            Action a = () => Console.WriteLine("Hello from original delegate");
            var horror = new Horror();
            a.Invoke();
            Action fix = () => horror.Fix(ref a);
            fix.BeginInvoke(fix.EndInvoke, null);
            Thread.Sleep(1000);
            horror.Clear();
            a.Invoke();
        }
    }
}