Skip to content

[BUG]panic: sync: RUnlock of unlocked RWMutex** —— Radius 认证高并发时进程崩溃 #184

@falseen

Description

@falseen

使用过程中发现一个高并发的bug,用AI修复了一下。以下文字来自AI:

🐞 Bug Report

panic: sync: RUnlock of unlocked RWMutex —— Radius 认证高并发时进程崩溃


1. 环境信息

项目 版本 / 描述
ToughRADIUS v8 (9a9edd1 及之后 commit)
Go 1.20.x
OS CentOS 7 / Rocky 9(多台同配置,仅高并发节点复现)
数据库 PostgreSQL 15

2. 复现步骤

  1. 使用默认配置启动 ToughRADIUS(radiusd.enabled=true,其余保持缺省)。

  2. 向 1812/UDP 持续发送 错误密码 的 Access-Request,触发 Reject 逻辑(脚本或真实 NAS 均可)。

  3. 数分钟后进程崩溃并被 systemd 重启,journalctl 输出首行:

    fatal error: sync: RUnlock of unlocked RWMutex
    
  4. 栈顶定位在 toughradius/radius_reject_delay.go:32, RejectItem.IsOver()


3. 实际日志(截取)

fatal error: sync: RUnlock of unlocked RWMutex

goroutine 77 [running]:
sync.fatal(...)
sync.(*RWMutex).rUnlockSlow(...)
sync.(*RWMutex).RUnlock(...)
github.com/talkincode/toughradius/v8/toughradius.(*RejectItem).IsOver
    toughradius/radius_reject_delay.go:32
github.com/talkincode/toughradius/v8/toughradius.(*RadiusService).CheckRadAuthError
    toughradius/errors.go:28
...

4. 预期行为

高并发认证失败场景下,进程应稳定运行,Reject 限速逻辑正常生效,而不是触发 panic。


5. 初步根因分析

  • 触发 commit:9a9edd1 Refactor RejectCache to use a read-write mutex for concurrent access
  • 代码将 Mutex 升级为 RWMutex,但保留了旧的 defer RUnlock(),又在逻辑分支里 手动 RUnlock() 后再次升级写锁,导致同一读锁被释放两次:
ri.Lock.RLock()
defer ri.Lock.RUnlock()          // 第一次解锁
...
ri.Lock.RUnlock()                // 第二次解锁 → panic
ri.Lock.Lock()                   // 升级写锁

RejectCache.GetItem() 中同样存在「读锁 + defer + 手动 RUnlock」的重复解锁。


6. 修复建议(已验证)

func (ri *RejectItem) IsOver(max int64) bool {
-   ri.Lock.RLock()
-   defer ri.Lock.RUnlock()
+   ri.Lock.RLock()

    if time.Since(ri.LastReject).Seconds() > 10 {
-       ri.Lock.RUnlock()
+       ri.Lock.RUnlock()              // 升级前显式释放
        ri.Lock.Lock()
-       defer ri.Lock.Unlock()
        if time.Since(ri.LastReject).Seconds() > 10 {
            atomic.StoreInt64(&ri.Rejects, 0)
        }
+       ri.Lock.Unlock()
        return false
    }
-   return atomic.LoadInt64(&ri.Rejects) > max
+   over := atomic.LoadInt64(&ri.Rejects) > max
+   ri.Lock.RUnlock()
+   return over
}

RejectCache.GetItem() 同理,去掉 defer RUnlock(),按需手动释放并在升级写锁后再 Unlock()


7. 影响范围

任何并发量较高、认证失败频繁的部署都会触发,导致进程循环崩溃并被 systemd/NSSM 重启。


8. 附件

  • 完整 panic stack trace
  • 最小复现脚本(可选提供)

完整的修改(仅供参考):falseen@def3c8d

感谢作者: 如需更多信息或验证补丁,请 @ 我 😊

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions