Недокументированные возможности недокументированных возможностей: Передача ref в другой поток

в 1:01, , рубрики: Без рубрики

Никогда не приходил в голову вопрос: "а как сохранить/передать в другой поток ссылку на поле?"? Логичным предположением будет «передам ref в метод и сохраню. Стоп, oh shi~». Да, ref не сохраняются (а ещё, нельзя использовать на них замыкания, так что создать функцию внутри такого метода и создать из её поток тоже не получится). Но зато можно превратить ref в TypedReference с помощью недокументированного ключевого слова __makeref. Увы, TypedReference нельзя напрямую сохранить в поле и не наследует от System.Object, так что каст привычными методами тоже невозможен (да и вообще на их использование наложена целая куча ограничений). Казалось бы, тупик. Но это ещё не всё — есть ещё RuntimeArgumentHandle, который обладает свойствами TypedReference, за одним исключением — после хитрого каста в System.Object его ещё можно использовать до тех пор, пока жив кадр стека, в котором он был создан. Об этом этот пост.

TypedReference

Сам TypedReference удивительнейшая вещица — в него можно завернуть ref и передать другой метод (т.е. из метода, один из параметров которого является ref'ом). Однако, методы этой структуры не позволяют нам задавать значение — NotSupportedException ожидает нас на вызове соответствующих методов. Но не беда — имеется ключевое слово __refvalue, которое позволяет не только получить значение, но и задать его. Но выглядит это довольно странно:

void Out(ref int someInt)
{
      Input(__makeref(someInt));
}

void Input(TypedReference @ref)
{
      int val = __refvalue(@ref, int);//Получаем значение
      __refvalue(@ref, int) = 0;//Задаём значение в someInt
}

При том, что тип задаётся ручками, скастить, например, int в string, не получится — проверка принадлежности типу все-таки проводится.
При этом всём, TypedReference тоже нельзя использовать в замыканиях — так что для того, чтобы создать что-то с замыканием на TypedReference тоже не получится.

RuntimeArgumentHandle

Является ничем иным, как params, только в профиль. По сути, представляет из себя некий список TypedReference'ов (доступ к которым производитс конструированием ArgIterator'а), а создаётся тоже… даже и не знаю как это описывать:

void Out(int something)
{
      Input(__arglist(something));
}

void Input(__arglist)
{
       new ArgIterator(__arglist);
}

При этом, ключевое слово __arglist нельзя использовать в делегатах при их обьявлении. Но RuntimeArgumentHandle можно (но только как параметры, TypedReference и RuntimeArgumentHandle нельзя возвращать из методов). __arglist() также нельзя использовать как аргумент для вызова делегата, но зато __arglist можно. Смысл этой несколько расплывчатой формулировки лучше подкрепить примером:

delegate void ArgWarrior(RuntimeArgumentHandle argh);

void Out(int something)
{
     (new ArgWarrior(u => { } ))(__arglist(someting));//не скомпилируется
     Input(new ArgWarrior(u => { } ), __arglist(someting);
}

void Input(ArgWarrior argh, __arglist)
{
       argh(__arglist);//а так можно
}

И вот я подобрался к ключевому моменту этого марлезонского балета: делегатам.

Манипуляции над _methodPtrAux как способ изысканных издевательств над делегатами

Недокументированные возможности недокументированных возможностей: Передача ref в другой поток _methodPtrAux — это четвёртое поле в любом делегируемом типе, которое сыграет тут ключевую роль. В чём суть? Суть в том, что _methodPtrAux хранит в себе указатель на уже jit'енный метод. Записав произвольный неуправляемый код по тому указателю, можно таким образом этот неуправляемый код выполнить. Но это тут не главное. Делегат остаётся пригодным к использованию даже после подмены значения _methodPtrAux, и при вызове его, управление перейдёт именно туда, куда указывает значение этого поля. Т.о., имея два делегата с разными входными параметрами, я могу заменить указатель из делегата a на указатель из делегата b. Даже если у них разный набор аргументов, всё сработает. Ключевым моментом будет так-же и то, что даже если различаются типы соответствующих аргументов, clr не забьёт тревогу — int будет скастен в string все желания будут исполнены, никто не уйдёт обиженным, или… RuntimeArgumentHandle будет преобразован в System.Object:

        delegate void Encast(RuntimeArgumentHandle @ref);
        delegate void Uncast(object @object);
        
       static void UseWith(Encast en, __arglist)
        {
            en(__arglist);
        }
        
       static object m_storedRef;
         
        static void Engage(ref object @object)
        {
             Encast en = new Encast(@ref => { });
             Uncast un = new Uncast(o =>
                {
                    m_storedRef = o;//сюда перейдёт управление после вызова <b>en</b>.
                });
                typeof(Encast).GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)[3].SetValue(en, typeof(Uncast).GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)[3].GetValue(un));//меняем указатель у en
                UseWith(en, __arglist(__makeref(@object))); //вызываем
        }

Как видно по лямбде, я сразу уже сохраняю полученное значение в статическое поле. Да, тут есть одно непонятное ограничение — если сохранять o в не-статическое поле то можно уронить clr (чтение-запись защищённой памяти). Даже если целевым полем будет не поле, а поле обьекта, что хранится в статическом поле (например, Dictionary) всё должно пройти гладко. Несколько чудно при этом аргумент лямбды выглядит в отладчике: при просмотре можно увидеть только "{object}" (без кавычек) и ничего более. Попытка извлечь тип или привести к String при этом ничего хорошего не сулит (можно уронить clr)

Скрытый текст
Зато если такой фокус провернуть с TypedReference, можно увидеть ещё кое-что интересное (это я оставляю на самостоятельное изучение читателям. Можно попробовать скастить в int, тоже любопытно).

Обратное преобразование производится аналогично. Сохранение же кадра стека производится с помощью мониторов:

      static object m_locker = new object();
      //...
     Monitor.Enter(m_locker);
     Monitor.Exit(m_locker);

m_locker уже заранее заблокирован из другого потока, так что выполнение приостанавливается, т.о. RuntimeArgumentHandle так и остаётся в стеке, не разрушаясь.
Полный код программы выглядит так:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace ThreadJiggler
{
    class Program
    {
        delegate void Encast(RuntimeArgumentHandle @ref);
        delegate void Uncast(object @object);
        static object m_storedRef;
        static object m_locker = new object();
        static bool m_useFlag;
        

        static void Main(string[] args)
        {
            object @v = "means "vendetta"";
            Victim1(ref @v);
            Console.WriteLine(@v);
        }

        static void UseWith(Encast en, __arglist)
        {
            en(__arglist);
        }

        static Thread m_someThread;

        static void Victim1(ref object @object)
        {
            Thread t = new Thread(() =>
            {
                Monitor.Enter(m_locker);
                {
                    for (; !m_useFlag; )
                    {
                        Thread.Sleep(10);
                    }

                    Encast en = new Encast(@ref =>
                    {
                        TypedReference tr = new ArgIterator(@ref).GetNextArg();
                        __refvalue( tr, object) = 0;
                    });

                    Uncast un = new Uncast(o => { m_storedRef = o; });
                    typeof(Uncast).GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)[3].SetValue(un, typeof(Encast).GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)[3].GetValue(en));
                    un(m_storedRef);
                }
                Monitor.Exit(m_locker);
            });
            t.IsBackground = false;
            t.Start();

            {
                Encast en = new Encast(@ref => { });
                Uncast un = new Uncast(o =>
                {
                    m_storedRef = o; m_useFlag = true;
                    Monitor.Enter(m_locker);
                    Monitor.Exit(m_locker);
                });
                typeof(Encast).GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)[3].SetValue(en, typeof(Uncast).GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)[3].GetValue(un));
                UseWith(en, __arglist(__makeref(@object)));
            }
        }
    }
}

В конце Main можно увидеть, что значение @v сменилось на 0.

Автор: 6opoDuJIo

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js