TOCTOU问题

TOCTOU(Time-Of-Check to Time-Of-Use)是一类典型的竞态条件问题,指程序在“检查资源状态”和“实际使用该资源”之间存在时间窗口,状态可能被其他线程或进程悄悄改掉,从而引发逻辑错误或安全漏洞。本文通过 Redisson 分布式锁unlock的实际案例,说明 TOCTOU 在业务代码中的具体表现。

post

Vitah Lin

1 min read
0

/

什么是TOC TOU

TOCTOU 是计算机安全和操作系统领域的一个术语,全称是 Time-Of-Check to Time-Of-Use,翻译过来就是“检查到使用的时间差问题”。

它是一种竞态条件(race condition)漏洞,指程序在检查某个状态和实际使用该状态之间存在时间间隙,攻击者可能利用这个间隙改变状态,从而导致安全问题。

  • Time-Of-Check (TOC):程序检查某个条件或状态,比如检查文件权限、检查内存是否可用。
  • Time-Of-Use (TOU):程序使用这个状态或资源,比如打开文件、写入数据。
  • TOCTOU问题:如果在TOC和TOU之间,资源状态被其他进程或线程修改,就可能引发错误或安全漏洞。

比如这段代码:

public void unlock(String key) {
	RLock lock = redissonClient.getLock(key);
	if (lock.isHeldByCurrentThread()) {
		lock.unlock();
  }
}

代码的逻辑很简单,就是检查当前线程是否持有锁,如果持有就释放锁。但是业务上原先加的锁是有设置超时时间的,这时候有时候会偶发出现:

attempt to unlock lock, not locked by current thread by node id: 64677d94-657d-4e01-928e-3b198e7b15bc thread-id: 348

就是因为在 unlock 的时候,锁已经过期了,就抛出异常。

解决思路

  1. 原子操作:尽量让检查和使用在同一原子操作中完成,比如操作系统提供的原子文件打开接口 open(O_CREAT | O_EXCL)。
  2. 锁机制:多线程环境用锁防止并发修改。
  3. 验证输入和状态:在使用前再验证,减少漏洞窗口。
  4. 避免依赖外部可修改状态:比如符号链接、共享文件、临时文件等。

比如当前可以使用 Redis 的 Lua 脚本来实现原子操作:

public void unlockAtomic(String key, String lockValue) {
    String luaScript =
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "   return redis.call('del', KEYS[1]) " +
        "else " +
        "   return 0 " +
        "end";

    redissonClient.getScript(StringCodec.INSTANCE)
        .eval(RScript.Mode.READ_WRITE, luaScript, RScript.ReturnType.INTEGER,
            Collections.singletonList(key), lockValue);
}

或者有一个更简单的处理方案,直接捕获 IllegalMonitorStateException 异常不处理:

public void unlock(String key) {
    RLock lock = redissonClient.getLock(key);
    try {
        lock.unlock();
    } catch (IllegalMonitorStateException e) {
        // 当前线程未持有锁,安全忽略或记录日志
        System.out.println("尝试释放锁失败,锁不是当前线程持有,忽略: " + key);
    }
}