Flier's Sky

天空,蓝色的天空,眼睛看不到的东西,眼睛看得到的东西

导航

http://www.blogcn.com/user8/flier_lu/index.html?id=2232621&run=.0999083

 cbrumme 在 Finalization 一文中多次提到了资源包装类生命期管理和句柄重用攻击的问题,其另外一篇文章 Lifetime, GC.KeepAlive, handle recycling 详细讨论了这个问题。

    首先考察一个外部资源包装类及其使用代码
以下为引用:

class C {
   IntPtr _handle;

   public ~C() { ... }

   Static void OperateOnHandle(IntPtr h) { ... }

   void m() {
      OperateOnHandle(_handle);
      ...
   }
   ...
}

class Other {
   void work() {
      if (something) {
         C aC = new C();

         aC.m();
         ...

         // most guess here
      } else {
         ...
      }
   }
}


    以 C++ 背景程序员的思路来看,aC 指向对象的生命期应该 if 语句块的末尾结束,无论是否增加一句 aC = null,aC 都会显式被标记为不再使用。如果是 C++ 代码的话,还会在 else 之前由编译器自动加入的 aC.~C() 析构函数调用等等。
    但是在 IL 代码一级,这个 "}" 实际上并不存在,它只是 C# 编译器增加的一个逻辑上的范围而已。对 JITer 所看到的 IL 代码这个层面,Other.work 函数中对 aC 的使用,在 aC.m() 调用之后就结束了。也就是说,GC 可以在 aC.m() 调用之后马上开始对 aC 的垃圾收集工作。
    有人会进一步猜测,aC 将在 C.m() 函数或 C.OperateOnHandle() 函数调用完成后可被回收。但实际上,在 m 函数中,this 指针在用来获取 _handle 内容后,就失去作用了。也就是说,在实际调用静态函数 OperateOnHandle 之前,aC 保存的对象就可以被回收。

    这样一来就会造成一种竞争条件的出现,在用户线程调用 C.OperateOnHandle() 函数处理 _handle 的句柄的同时,后台 Finalizer 线程可能已经在调用 C 类型的 Finalize 函数关闭 _handle 句柄了。出现这种问题的根本原因在于代码打破了对外部资源句柄封装的透明性,封装类 C 和被封装的句柄 _handle 的生命周期被分离开来。导致此问题的原因,还可以由于将 _handle 通过函数返回给最终使用者或者放入某个静态变量中。

    现有情况下一个解决办法是在 OperateOnHandle 函数调用后添加一个 GC.KeepAlive(this) 调用,向 JIT 和 GC 标记当前对象的生命期将显式被延续到其所封装外部资源句柄的生命期之后。此函数不进行任何操作,只是 touch 目标对象一下,表示我还需要使用它,呵呵。
    而这种将生命期维护工作交给最终用户来完成的策略,某种程度上将大大增加潜在问题出现的可能性,必须以其他替代机制保护这种被分离生命期的外部资源对象。

    v1.1 内部处理这种问题的解决方法是通过提供诸如 System.Threading.__HandleProtector 这样的内部类。当外部句柄从包装类的生命期中分离出去时,通过新增保护机制来单独维护外部对象的生命期,从而彻底分离两者。例如 System.Threading.WaitHandle 类型的 Handle 属性就通过
以下为引用:

namespace System.Threading
{
  class WaitHandle
  {
    public virtual IntPtr Handle
    {
      get
      {
        if (this.waitHandleProtector != null)
        {
          return this.waitHandleProtector.Handle;
        }
        return WaitHandle.InvalidHandle;
      }
      set
      {
        this.waitHandleProtector = ((value == WaitHandle.InvalidHandle) ? null : new __WaitHandleHandleProtector(value));
        this.waitHandle = value;
      }
    }
  }
}


    类似的问题还存在于提供了 IDisposable 接口的包装类。当一个线程在调用 Dispose 方法时,另一个线程可能正在使用这个资源。GC.KeepAlive 是无法完全解决这类问题的,而通过大范围的锁来彻底解决,则在性能和功能上是不现实的。
    而这类问题还可能导致前面所说的句柄重用攻击的安全漏洞。如恶意代码可以打开一个具有足够权限的文件,然后同时调用 Read 和 Dispose 方法。而如果恰好发生竞争条件时,另外一个重要文件被打开并重用了此句柄,则本不应被授权的读操作将利用竞争条件被执行。者就是句柄重用攻击。
    要完全解决此问题,在现有架构下需要增加自动对资源引用次数的追踪,通过计数器来跟踪句柄的使用情况,防止竞争条件的发生。但这样做会带来性能上的代价,以及复杂的 unmanaged 资源跟踪机制,并且需要由 CLR 自动或用户手工来确定哪些资源是需要跟踪的。这显然并不现实可行,并且会因为另外的如 unsafe 代码等问题导致连锁问题。
    
    为此 Whidbey 提供了上述的 SafeHandle 类型,从其继承出来的子类,如 SafeWaitHandle 类型,防止上述问题的发生。如 CLR 2.0 中 WaitHandle.Handle 属性的处理代码改成下面的形式:
以下为引用:

namespace System.Threading
{
  class WaitHandle
  {
    protected virtual void Dispose(bool explicitDisposing)
    {
      if (this.safeWaitHandle != null)
      {
        this.safeWaitHandle.Close(); 
      } 
    }
    public virtual IntPtr Handle
    {
      get
      {
        if (this.safeWaitHandle != null)
        {
          return this.safeWaitHandle.DangerousGetHandle();
        }
        return WaitHandle.InvalidHandle;
      }
      set
      {
        if (value == WaitHandle.InvalidHandle)
        {
          this.safeWaitHandle.SetHandleAsInvalid();
          this.safeWaitHandle = null;
        }
        else
        {
          this.safeWaitHandle = new SafeWaitHandle(value, true);
        }
        this.waitHandle = value;
      }
    }
  }
}