不安全的代码对安全代码有影响吗?

17
据我所知,将一个方法标记为不安全会禁用CLR在该代码上的某些检查,但这是否会对其余安全系统产生任何影响,除了DLL/EXE无法在不受信任的环境中运行外?
具体来说:
1. 因为标记为不安全,是否有任何安全检查不能在完整的dll上工作? 2. 如果一个DLL被标记为不安全,但是标记为不安全的方法实际上没有被调用,那么这和将DLL标记为安全是一样的吗? 3. 在将不安全代码保持在单独的DLL中方面,是否存在任何运行时优势?
我遇到了在64位Windows上重绘嵌套控件的问题,详见这里,而解决方案(看起来可行的方案)涉及使用不安全代码,我想了解添加此代码对我的项目的影响。
2个回答

8

不安全的代码可以 破坏托管堆。因此,运行在同一进程中的任何内容都可能受到影响,包括所有其他和潜在的所有其他AppDomains


更新

以下是一个示例:http://blogs.msdn.com/b/tess/archive/2006/02/09/net-crash-managed-heap-corruption-calling-unmanaged-code.aspx


更新2

认真编写的不安全代码是不好的吗?

不是。.NET框架本身就有很多不安全的代码,例如System.String中的一个:

public static unsafe string Copy(string str)
{
    if (str == null)
    {
        throw new ArgumentNullException("str");
    }
    int length = str.Length;
    string str2 = FastAllocateString(length);
    fixed (char* chRef = &str2.m_firstChar)
    {
        fixed (char* chRef2 = &str.m_firstChar)
        {
            wstrcpyPtrAligned(chRef, chRef2, length);
        }
    }
    return str2;
}

1
我理解并接受如果不安全的代码存在漏洞会带来的风险,但是假设不安全的代码完美运行(或从未执行),那么我的整个系统是否会变得不安全? - sgmoore
没错。他们之所以把“unsafe”关键字设计成这样,是有原因的。 - Harry Steinhilber
3
@Harry: 实际上,即使微软的营销团队不希望团队称其为“不安全”(http://www.artima.com/intv/choices2.html),我很高兴他们称其为“不安全”。 - Steven
2
“安全”的代码也有可能破坏堆,因为安全的代码可以使用dllimport,并将无效指针(包括“new IntPtr(666)”)传递给该入口点。 - David Jeske

2
你的问题的答案是:"unsafe"关键字并不意味着“不安全”,而是“潜在不安全”。编译器和框架不能保证其安全性,需要你确保代码不会对内存执行不安全的读写操作。
我强烈建议你遵循链接文章中提供的建议:
1)重新设计应用程序,减少容器数量和嵌套层数。
如果你只是为了控制排列而使用容器,请编写自己的容器来完成所有单层排列。
更新:
你可以修改那篇文章中的代码,使其不使用指针(即不需要unsafe关键字)。请注意,这将现在需要进行编组,这意味着额外的复制。这可能是一件好事,因为原始代码正在将一个从操作系统传递到BeginInvoke的WINDOWPOS指针,而该指针并未在操作系统生成它的同一调度事件期间执行。换句话说,那段代码已经有异味了。
internal class MyTabPage : TabPage
{
    private const int WM_WINDOWPOSCHANGING = 70;
    private const int WM_SETREDRAW = 0xB;
    private const int SWP_NOACTIVATE = 0x0010;
    private const int SWP_NOZORDER = 0x0004;
    private const int SWP_NOSIZE = 0x0001;
    private const int SWP_NOMOVE = 0x0002;

    [DllImport("User32.dll", CharSet = CharSet.Auto)]
    extern static int SendMessage(HandleRef hWnd, int msg, int wParam, int lParam);

    [DllImport("User32.dll", ExactSpelling = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
    extern static bool SetWindowPos(HandleRef hWnd, HandleRef hWndInsertAfter,
    int x, int y, int cx, int cy, int flags);

    [StructLayout(LayoutKind.Sequential)]
    private class WINDOWPOS
    {
        public IntPtr hwnd;
        public IntPtr hwndInsertAfter;
        public int x;
        public int y;
        public int cx;
        public int cy;
        public int flags;
    };

    private delegate void ResizeChildDelegate(WINDOWPOS wpos);

    private void ResizeChild(WINDOWPOS wpos)
    {
        // verify if it's the right instance of MyPanel if needed
        if ((this.Controls.Count == 1) && (this.Controls[0] is Panel))
        {
            Panel child = this.Controls[0] as Panel;

            // stop window redraw to avoid flicker
            SendMessage(new HandleRef(child, child.Handle), WM_SETREDRAW, 0, 0);

            // start a new stack of SetWindowPos calls
            SetWindowPos(new HandleRef(child, child.Handle), new HandleRef(null, IntPtr.Zero),
            0, 0, wpos.cx, wpos.cy, SWP_NOACTIVATE | SWP_NOZORDER);

            // turn window repainting back on 
            SendMessage(new HandleRef(child, child.Handle), WM_SETREDRAW, 1, 0);

            // send repaint message to this control and its children
            this.Invalidate(true);
        }
    }

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == WM_WINDOWPOSCHANGING)
        {
            WINDOWPOS wpos = new WINDOWPOS();
            Marshal.PtrToStructure(m.LParam, wpos);

            Debug.WriteLine("WM_WINDOWPOSCHANGING received by " + this.Name + " flags " + wpos.flags);

            if (((wpos.flags & (SWP_NOZORDER | SWP_NOACTIVATE)) == (SWP_NOZORDER | SWP_NOACTIVATE)) &&
            ((wpos.flags & ~(SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE)) == 0))
            {
                if ((wpos.cx != this.Width) || (wpos.cy != this.Height))
                {
                    BeginInvoke(new ResizeChildDelegate(ResizeChild), wpos);
                    return;
                }
            }
        }

        base.WndProc(ref m);
    }
}

Note: WINDOWPOS从值类型更改为引用类型是有意的。使用引用类型可以将副本数量减少到仅一个(初始marshal)(**)。

再次更新:我刚刚注意到,代码最初使p/invoke声明为公共的。永远不要在类外部暴露p/invoke (*).编写调用私有p/Invoke声明的托管方法,如果您的意图是公开提供的功能; 在这种情况下不是这样,p/Invoke是严格内部的。

(*) 好吧,有一个例外。您正在创建一个NativeMethodsUnsafeNativeMethods等,这是FxCop建议进行p/invoke的方式。

更新

(**) 我被要求(在其他地方)精确描述为什么在此处使用引用类型更好,因此我在此添加了该信息。我被问到的问题是:“这不会增加内存压力吗?”

如果WINDOWPOS是值类型,则会发生以下事件序列:

1)从非托管内存复制到托管内存

WINDOWPOS wpos = Marshal.PtrToStructure(m.LParam, typeof(WINDOWPOS));

2) 第二份拷贝?

BeginInvoke(new ResizeChildDelegate(ResizeChild), wpos);

等等!BeginInvoke的签名是(Delegate, params object[])。这意味着wpos将被装箱。因此,在此处发生了第二个副本:装箱操作。 BeginInvoke会将委托和object[]添加到调用列表中,并发布注册的窗口消息。当消息泵从队列中删除该消息时,将使用object[]参数调用委托。
3)为ResizeChild调用进行拆箱和复制。
此时可以看出,复制的数量甚至不是问题。它被转换为引用类型(装箱),这意味着最好一开始就将其作为引用类型处理。

1
谢谢您的回答。我仍然希望找到一种避免使用不安全代码的方法,但是还没有找到可靠的方法(至今为止)。重新设计应用程序并不容易,因为它涉及多个用户控件,在各个地方都有重复使用。但是,我认为您(和其他人)忽略了我试图(显然失败了)提出的问题的要点,即是否将dll标记为unsafe,并将方法标记为unsafe对dll或应用程序中的其余代码产生任何影响,即使我们假设实际的不安全代码从未出错。 - sgmoore
@sgmoore:我更新了我的答案,展示了如何从代码中删除指针的使用。这并没有回答问题,但我不知道使用/unsafe开关所涉及的细节。 - Tergiver
谢谢,这似乎起作用了。显然我还要进行更多的测试并尝试弄清楚它的功能。例如,如果ReSizeChild没有任何操作,则调用ReSizeChild而不调用base.WndProc(ref m)的后果。 - sgmoore
1
此外,“安全”的代码只是“潜在的安全”,因为安全的代码可以dllimport,以及处理或修改IntPtr,这些都会传递给其他人的dllimport。这两者都可能导致崩溃或内存损坏。这就引出了一个问题:“安全”到底意味着什么? - David Jeske
1
@Tergiver - David Jeske已经证明了Verifiable为什么不能保证内存写入是有效的。 - hoodaticus
显示剩余3条评论

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