如何隔离依赖项以防止传递性依赖解析?

9

我正在开发一个应用程序,为客户提供插件接口来开发其在应用程序中的逻辑。这些插件会在运行时进行动态加载。我们为插件提供了一个清晰的 C 接口,以便尽可能地实现可移植性。然而,我们最近发现了一个问题,涉及到过渡性依赖:当一个插件链接到自己的依赖库时,而该依赖库又恰好是应用程序的依赖库时,只有与应用程序一起发布的版本会被加载。

因此,在以下配置中,lib_b.dll 是插件,它使用 lib_a.dll 作为私有依赖库。然而,由于可执行文件也链接到了同一库的不同版本,所以它们的版本不会被选择。

    +----------------------+              +-------------------------------+
    |                      | LoadLibrary  |                               |
    | Executable.exe ------+--------------+--> plugins                    |
    |  |                   |              |     |                         |
    |  +--> lib_a.dll (v1) |              |     +--> lib_b.dll            |
    |                      |              |           |                   |
    +----------------------+              |           +--> lib_a.dll (v2) |
                                          |                               |
                                          +-------------------------------+

我正在寻找一种解决方案,可以将我的应用程序的地址空间和依赖项的符号隔离开来。这个想法是让Executable只关心它在运行时从插件中加载的符号,而不关心插件内部使用了什么。
我们像这样进行load_library:
HMODULE h = ::LoadLibraryExA(".../plugins/library_b.dll", 
    NULL, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR);

插件中唯一有趣的地方就是这样实现的。
callback_type do_the_thing_in_b = GetProcAddress(h, "do_the_thing_in_b");
int answer = do_the_thing_in_b(3.14, 42);

更新

该应用程序可以随时重新编译或更改,但在编译时我们对插件没有任何信息。想法是客户可以创建自己的插件并将其放置在那里。

我们也无法修改这些插件。我们可以扫描该目录并执行操作,但我们所能做的仅限于此。我们不能重新编译它们或决定它们的依赖结构。

这些插件通过接口库与我们的可执行文件进行链接,并直接从可执行文件调用函数。


https://learn.microsoft.com/en-us/windows/win32/sbscs/about-isolated-applications-and-side-by-side-assemblies - Hans Passant
2
你看过这个答案吗?https://dev59.com/Lb7pa4cB1Zd3GeqP3qxe - linuxfever
2
要不要运行一个加载插件并与之进行进程间通信的服务/独立应用程序呢? - Daniil Vlasenko
@DaniilVlasenko 插件代码与我们的可执行文件链接,并调用函数... - sorush-r
也许不是你想要的答案,但你可以使用COM(https://learn.microsoft.com/en-us/windows/win32/com/component-object-model--com--portal),它在Windows中被广泛使用。它正是为这种类型的目的而设计的。 - Simon Mourier
显示剩余13条评论
2个回答

0

总结:

  • 您有一个具有定义的插件接口的应用程序
  • 您定义并控制该接口
  • 插件依赖于能够调用应用程序内部的一些函数
  • 插件和应用程序可能对依赖项的正确版本有不同的观点,而应用程序的观点会胜出
  • 插件不会调用其他插件中的函数

我认为你对“隔离地址空间”一词的理解很到位,正如Daniil Vlasenko在评论中建议的那样。这也是FireFox现在在选项卡中所做的;每个选项卡都是一个独立的进程。

如果您控制接口,您就有能力将主可执行文件分离为一个进程,并拥有一个可以加载单个插件DLL(及其依赖项)的薄型框架进程。您会为每个插件启动一个这样的框架。进程将通过某种机器内部的RPC进行通信。这一切都假设数据结构不会共享,除非通过函数调用和返回来实现。

那会是什么样子呢?

RPC(例如GPB-RPC)感觉不太合适,因为你需要一个RPC单向传输来让可执行文件调用插件,但同时也需要允许插件反过来调用可执行文件中的函数。
我会使用类似ZeroMQ这样的工具作为主要可执行文件和插件之间的传输方式,并使用类似GPB这样的协议来构建消息,其中包括:1)指示要调用的函数是什么,以及2)该函数的参数是什么。整个流程大致如下:
  1. 每个插件都有2个ZMQ REQ/REP socket对,一个对从可执行文件到插件桥接进程的调用(call forward),另一个在相反方向上运行(call back)。每个进程最终都有一个REQ和一个REP socket。
  2. 在主可执行文件中制定一条消息,将其发送给插件桥接进程以调用提供的函数。
  3. 通过主可执行文件的call forward REQ socket发送该消息,然后继续轮询call forward REQ socket和call back REP socket。这使得主可执行文件可以收到返回值,或者接收到对其自身函数的请求来执行。
  4. 桥接进程通过其call forward REP socket接收到该消息,对其进行解码,并在插件DLL中调用相关函数。
  5. 插件函数可能想要调用主可执行文件中的函数。您必须在桥接进程中实现所有这些函数的存根版本。薄桥接形式中的存根函数组合一个包含调用参数的消息,并将其通过自己的call back REQ socket发送回主可执行文件,然后在call back REQ socket上的zmq_recv()上阻塞。
  6. 与此同时,主可执行文件在轮询其call forward REQ和call back REP socket时,被ZMQ告知call back REP socket上有一条消息。它读取此消息,执行指定的函数,并收集结果。
  7. 结果通过call back REP socket发送回桥接进程,主可执行文件继续轮询两个socket。
  8. 在zmq_recv上阻塞的桥接进程接收到结果消息,并将结果返回给DLL插件中的调用函数。
  9. 插件函数本身最终完成,并向桥接进程返回结果。
  10. 这个最终结果被封装成一条消息,通过桥接的call forward REP socket发送回主可执行文件。
  11. 主可执行文件这次得到通知,在其call forward REQ socket上有一条准备好的消息-来自插件的回复。
  12. 读取此消息,并将数据返回给主可执行文件中想要调用插件的部分。
这样做的好处是:
- 主可执行文件可以调用插件中的函数 - 被调用的函数可以任意次数地调用主可执行文件提供的函数(从0次到很多次) - 插件函数的结果可以返回给主可执行文件 - 插件DLL和主可执行文件可以存在于不同的进程中,并加载它们各自所需的依赖项。
ZMQ将会非常有用,因为听起来你的主可执行文件和插件之间并没有简单的客户端/服务器或RPC关系;它们之间有一些相互依赖。ZMQ是Actor模型,可以实现上述描述的这种模式。
使用ZeroMQ的一个额外好处是,掌握了这个技术后,插件可以轻松地部署在完全不同的机器上,或者是组合部署(即一些本地的,一些远程的,一些运行在世界另一边的Linux上等)。
显然,如果存在共享数据结构,拥有两个独立的进程是无法解决问题的,尽管我想这个问题可以通过将它们放置在共享内存中来解决。但是,所有插件的shim进程都必须与主可执行文件在同一台机器上。
如果事情变得更加多线程,我认为上面概述的模式并不会有太大改变。你可能希望在消息中添加字段来指示正在发生的事情。如果插件正在操作主可执行文件创建的信号量、互斥锁等,可能会变得更加困难一些。

0
在加载dll时,Windows总是调用函数RtlDosApplyFileIsolationRedirection_Ustr。它是导出的,所以很容易被挂钩。通过这个API,我们可以重定向(替换)dll名称。
所以首先尝试挂钩这个API:
#ifdef _X86_

#pragma warning(disable: 4483) // Allow use of __identifier
#define __imp_RtlDosApplyFileIsolationRedirection_Ustr __identifier("_imp__RtlDosApplyFileIsolationRedirection_Ustr@36")
#endif

EXTERN_C extern PVOID __imp_RtlDosApplyFileIsolationRedirection_Ustr;

NTSTATUS
NTAPI
hook_RtlDosApplyFileIsolationRedirection_Ustr(_In_ ULONG Flags,
                                              _In_ PUNICODE_STRING OriginalName,
                                              _In_ PUNICODE_STRING Extension,
                                              _Out_opt_ PUNICODE_STRING StaticString,
                                              _Out_opt_ PUNICODE_STRING DynamicString,
                                              _Out_opt_ PUNICODE_STRING *NewName,
                                              _Out_opt_ PULONG NewFlags,
                                              _Out_opt_ PSIZE_T FilePathLength,
                                              _Out_opt_ PSIZE_T MaxPathSize);

ULONG dwError = DetourTransactionBegin();
if (NOERROR == dwError)
{
    //++ optional
    DetourThread* pti = 0;
    SuspendThreads(&pti);
    //--optional

    dwError = DetourAttach(&__imp_RtlDosApplyFileIsolationRedirection_Ustr,
         hook_RtlDosApplyFileIsolationRedirection_Ustr);

    dwError = NOERROR != dwError ? DetourTransactionAbort() : DetourTransactionCommit();

    //++optional
    Free(pti);
    //--optional
}

实现SuspendThreads可选)可以如下所示:
struct DetourThread
{
    DetourThread *      pNext;
    HANDLE              hThread;
};

void Free(_In_ DetourThread* next)
{
    if (DetourThread* pti = next)
    {
        do 
        {
            next = pti->pNext;

            NtClose(pti->hThread);

            delete pti;

        } while (pti = next);
    }
}

NTSTATUS SuspendThreads(_Out_ DetourThread** ppti)
{
    DetourThread* pti = 0;
    HANDLE ThreadHandle = 0, hThread;
    NTSTATUS status;
    BOOL bClose = FALSE;

    HANDLE UniqueThread = (HANDLE)GetCurrentThreadId();

loop:
    status = NtGetNextThread(NtCurrentProcess(), ThreadHandle, 
        THREAD_QUERY_LIMITED_INFORMATION|THREAD_SUSPEND_RESUME|THREAD_GET_CONTEXT|THREAD_SET_CONTEXT, 
        0, 0, &hThread);

    if (bClose)
    {
        NtClose(ThreadHandle);
        bClose = FALSE;
    }

    if (0 <= status)
    {
        ThreadHandle = hThread;

        THREAD_BASIC_INFORMATION tbi;

        if (0 <= (status = NtQueryInformationThread(hThread, ThreadBasicInformation, &tbi, sizeof(tbi), 0)))
        {
            if (tbi.ClientId.UniqueThread == UniqueThread)
            {
                bClose = TRUE;
                goto loop;
            }

            if (NOERROR == (status = DetourUpdateThread(hThread)))
            {
                status = STATUS_NO_MEMORY;

                if (DetourThread* next = new DetourThread)
                {
                    next->hThread = hThread;
                    next->pNext = pti;
                    pti = next;
                    goto loop;
                }

                ResumeThread(hThread);
            }
        }

        if (status == STATUS_THREAD_IS_TERMINATING)
        {
            bClose = TRUE;
            goto loop;
        }

        NtClose(hThread);
    }

    switch (status)
    {
    case STATUS_NO_MORE_ENTRIES:
    case STATUS_SUCCESS:
        *ppti = pti;
        return STATUS_SUCCESS;
    }

    Free(pti);

    *ppti = 0;
    return status;
}

the DetourUpdateThread suspend and save thread handles in DetourThread list. and resume it in DetourTransactionAbort or DetourTransactionCommit. but it not close saved hThread. as result need by self mantain additional list of threads, for close it handles.. (Free)

ok. let we hook RtlDosApplyFileIsolationRedirection_Ustr. now we need implement hook_RtlDosApplyFileIsolationRedirection_Ustr

let we have next api:

// set path to some plugin (A) folder. 
// pszPluginPath - relative path. like plugin/A/

BOOL SetPluginPath(_In_ PCWSTR pszPluginPath);

// return full path to current plugin (A) folder - some like */plugin/A/

PCWSTR AcquirePluginPath();

void ReleasePluginPath();

// called once on start
BOOL InitPluginPath();

// called once on exit
void FreePluginPath();

将插件加载代码放在下面的代码中:
    if (SetPluginPath(L"plugins/A/"))
    {
        LoadLibraryW(L"some-plugin.dll");
        RemovePluginPath();
    }

假设./plugin/是应用程序文件夹内的一个文件夹(exe所在的位置),它包含每个插件的子文件夹(AB,..)。
/plugin
   /A
   /B

所以,通过这段代码,我们尝试加载./plugin/A/some-plugin.dll

如果some-plugin.dll有静态依赖(或在dll入口点调用LoadLibrary - 这是可以的),依赖于lib-Y.dll并且存在./plugin/A/lob-Y.dll文件 - 我们将尝试加载./plugin/A/lob-Y.dll。即使./lib-Y.dll和/或./plugin/B/lib-Y.dll已经在进程中加载。

NTSTATUS
NTAPI
hook_RtlDosApplyFileIsolationRedirection_Ustr(_In_ ULONG Flags,
                                              _In_ PUNICODE_STRING OriginalName,
                                              _In_ PUNICODE_STRING Extension,
                                              _Out_opt_ PUNICODE_STRING StaticString,
                                              _Out_opt_ PUNICODE_STRING DynamicString,
                                              _Out_opt_ PUNICODE_STRING *NewName,
                                              _Out_opt_ PULONG NewFlags,
                                              _Out_opt_ PSIZE_T FilePathLength,
                                              _Out_opt_ PSIZE_T MaxPathSize)
{
    if (DynamicString)
    {
        BOOLEAN fOk = FALSE;
        WCHAR lpLibFileName[MAX_PATH], *lpFilePart = 0;

        if (PCWSTR pszPluginPath = AcquirePluginPath())
        {
            if (!wcscpy_s(lpLibFileName, _countof(lpLibFileName), pszPluginPath))
            {
                size_t s = wcslen(lpLibFileName);

                lpFilePart = lpLibFileName + s;

                int len = swprintf_s(lpFilePart, _countof(lpLibFileName) - s, L"%wZ", OriginalName);

                if (0 < len)
                {
                    static const UNICODE_STRING dot = RTL_CONSTANT_STRING(L".");
                    USHORT u;
                    if (0 > RtlFindCharInUnicodeString(0, OriginalName, &dot, &u))
                    {
                        swprintf_s(lpFilePart + len, _countof(lpLibFileName) - s - len, L"%wZ", Extension);
                    }       

                    fOk = RtlDoesFileExists_U(lpLibFileName);
                }
            }
        }

        ReleasePluginPath();

        if (fOk)
        {
            if (RtlCreateUnicodeString(DynamicString, lpLibFileName))
            {
                if (NewName)
                {
                    *NewName = DynamicString;
                }

                if (NewFlags)
                {
                    *NewFlags = 0;
                }

                if (FilePathLength)
                {
                    *FilePathLength = lpFilePart - lpLibFileName;
                }

                if (MaxPathSize)
                {
                    *MaxPathSize = _countof(lpLibFileName);
                }

                return STATUS_SUCCESS;
            }

            return STATUS_NO_MEMORY;
        }
    }

    return RtlDosApplyFileIsolationRedirection_Ustr(Flags,
        OriginalName,
        Extension,
        StaticString,
        DynamicString,
        NewName,
        NewFlags,
        FilePathLength,
        MaxPathSize);
}

最后 - 插件路径 API 的实现:
SRWLOCK g_lock = RTL_SRWLOCK_INIT;
ULONG g_cchMaxPlugin = 0;
PWSTR g_pszPluginPath = 0, g_pszPluginRelativePath = 0, g_pszPluginName = 0;

void FreePluginPath()
{
    if (g_pszPluginPath)
    {
        if (g_pszPluginName)
        {
            __debugbreak();
        }

        delete [] g_pszPluginPath;
    }
}

BOOL InitPluginPath()
{
    enum { buf_size = MAXSHORT + 1 };

    if (PWSTR psz = new(nothrow) WCHAR[buf_size])
    {
        if (ULONG cch = GetModuleFileName(0, psz, buf_size))
        {
            if (NOERROR == GetLastError())
            {
                PWSTR FileName = psz + cch;
                g_cchMaxPlugin = buf_size - cch;

                do 
                {
                    switch (*--FileName)
                    {
                    case '\\':
                    case '/':
                        g_pszPluginPath = psz;
                        g_pszPluginRelativePath = FileName + 1;
                        return TRUE;

                    }
                } while (g_cchMaxPlugin++, --cch);
            }
        }
        delete [] psz;
    }

    return FALSE;
}

BOOL SetPluginPath(_In_ PCWSTR pszPluginPath)
{
    SIZE_T cch = wcslen(pszPluginPath);

    PWSTR pszPluginName = g_pszPluginRelativePath + cch;

    if (++cch > g_cchMaxPlugin)
    {
        return FALSE;
    }

    AcquireSRWLockExclusive(&g_lock);

    memcpy(g_pszPluginRelativePath, pszPluginPath, cch * sizeof(WCHAR));

    g_pszPluginName = pszPluginName;

    ReleaseSRWLockExclusive(&g_lock);

    return TRUE;
}

void ReleasePluginPath()
{
    ReleaseSRWLockShared(&g_lock);
}

PCWSTR AcquirePluginPath()
{
    AcquireSRWLockShared(&g_lock);
    return g_pszPluginName ? g_pszPluginPath : 0;
}

void RemovePluginPath()
{
    AcquireSRWLockExclusive(&g_lock);
    g_pszPluginName = 0;
    ReleaseSRWLockExclusive(&g_lock);
}

在开始时,我们必须调用InitPluginPath()函数,在退出时调用FreePluginPath()函数。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接