• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lunarmodules / copas / 30211738911

26 Jul 2026 05:02PM UTC coverage: 85.343% (+0.1%) from 85.229%
30211738911

push

github

Tieske
fix(semaphore): take() must reject requests below 1 or NaN

A negative `requested` on the fast path (`count = count - requested`)
turned a take into a give, inflating `count` past `max` with no check
at all (eg. take(-100) on a max=5 semaphore left count at 100). A NaN
request would queue forever, since `count >= NaN` is always false and
can never be satisfied, deadlocking that entry and everyone behind it
in the FIFO queue. Reject both instead.

2 of 2 new or added lines in 1 file covered. (100.0%)

9 existing lines in 5 files now uncovered.

1444 of 1692 relevant lines covered (85.34%)

55080.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.68
/src/copas/lock.lua
1
local copas = require("copas")
12✔
2
local gettime = copas.gettime
12✔
3

4
local coroutine_running = coroutine.running
12✔
5
if _VERSION=="Lua 5.1" and not jit then     -- obsolete: only for Lua 5.1 compatibility
12✔
UNCOV
6
  coroutine_running = require("coxpcall").running
2✔
7
end
8

9
local DEFAULT_TIMEOUT = 10
12✔
10

11
local lock = {}
12✔
12
lock.__index = lock
12✔
13

14

15
-- registry, locks indexed by the coroutines using them.
16
local registry = setmetatable({}, { __mode="kv" })
12✔
17

18

19

20
--- Creates a new lock.
21
-- @param seconds (optional) default timeout in seconds when acquiring the lock (defaults to 10),
22
-- set to `math.huge` to have no timeout.
23
-- @param not_reentrant (optional) if truthy the lock will not allow a coroutine to grab the same lock multiple times
24
-- @return the lock object
25
function lock.new(seconds, not_reentrant)
12✔
26
  local timeout = tonumber(seconds or DEFAULT_TIMEOUT) or -1
66✔
27
  if timeout < 0 then
66✔
28
    error("expected timeout (1st argument) to be a number greater than or equal to 0, got: " .. tostring(seconds), 2)
×
29
  end
30
  return setmetatable({
121✔
31
            timeout = timeout,
66✔
32
            not_reentrant = not_reentrant,
66✔
33
            queue = {},
66✔
34
            q_tip = 0,  -- index of the first in line waiting
55✔
35
            q_tail = 0, -- index where the next one will be inserted
55✔
36
            owner = nil, -- coroutine holding lock currently
55✔
37
            call_count = nil, -- recursion call count
55✔
38
            errors = setmetatable({}, { __mode = "k" }), -- error indexed by coroutine
66✔
39
          }, lock)
99✔
40
end
41

42

43

44
do
45
  local destroyed_func = function()
46
    return nil, "destroyed"
24✔
47
  end
48

49
  local destroyed_lock_mt = {
12✔
50
    __index = function()
51
      return destroyed_func
24✔
52
    end
53
  }
54

55
  --- destroy a lock.
56
  -- Releases all waiting threads with `nil+"destroyed"`
57
  function lock:destroy()
12✔
58
    --print("destroying ",self)
59
    for i = self.q_tip, self.q_tail do
1,596✔
60
      local co = self.queue[i]
1,548✔
61
      self.queue[i] = nil
1,548✔
62

63
      if co then
1,548✔
64
        self.errors[co] = "destroyed"
1,494✔
65
        --print("marked destroyed ", co)
66
        copas.wakeup(co)
1,494✔
67
      end
68
    end
69

70
    if self.owner then
48✔
71
      self.errors[self.owner] = "destroyed"
48✔
72
      --print("marked destroyed ", co)
73
    end
74
    self.queue = {}
48✔
75
    self.q_tip = 0
48✔
76
    self.q_tail = 0
48✔
77
    self.destroyed = true
48✔
78

79
    setmetatable(self, destroyed_lock_mt)
48✔
80
    return true
48✔
81
  end
82
end
83

84

85
local function timeout_handler(co)
86
  local self = registry[co]
1,512✔
87
  if not self then
1,512✔
88
    return
×
89
  end
90

91
  for i = self.q_tip, self.q_tail do
188,262✔
92
    if co == self.queue[i] then
188,262✔
93
      self.queue[i] = nil
1,512✔
94
      self.errors[co] = "timeout"
1,512✔
95
      --print("marked timeout ", co)
96
      copas.wakeup(co)
1,512✔
97
      return
1,512✔
98
    end
99
  end
100
  -- if we get here, we own it currently, or we finished it by now, or
101
  -- the lock was destroyed. Anyway, nothing to do here...
102
end
103

104

105
--- Acquires the lock.
106
-- If the lock is owned by another thread, this will yield control, until the
107
-- lock becomes available, or it times out.
108
-- If `timeout == 0` then it will immediately return (without yielding).
109
-- @param timeout (optional) timeout in seconds, defaults to the timeout passed to `new` (use `math.huge` to have no timeout).
110
-- @return wait-time on success, or nil+error+wait_time on failure. Errors can be "timeout", "destroyed", or "lock is not re-entrant"
111
function lock:get(timeout)
12✔
112
  local co = coroutine_running()
4,614✔
113
  local start_time
114

115
  -- is the lock already taken?
116
  if self.owner then
4,614✔
117
    -- are we re-entering?
118
    if co == self.owner and not self.not_reentrant then
3,036✔
119
      self.call_count = self.call_count + 1
×
120
      return 0
×
121
    end
122

123
    timeout = timeout or self.timeout
3,036✔
124
    if timeout == 0 then
3,036✔
125
      return nil, "timeout", 0
12✔
126
    end
127

128
    self.queue[self.q_tail] = co
3,024✔
129
    self.q_tail = self.q_tail + 1
3,024✔
130

131
    -- set up timeout
132
    registry[co] = self
3,024✔
133
    copas.timeout(timeout, timeout_handler)
3,024✔
134

135
    start_time = gettime()
3,024✔
136
    copas.pauseforever()
3,024✔
137

138
    local err = self.errors[co]
3,018✔
139
    self.errors[co] = nil
3,018✔
140
    registry[co] = nil
3,018✔
141

142
    --print("released ", co, err)
143
    if err ~= "timeout" then
3,018✔
144
      copas.timeout(0)
1,506✔
145
    end
146
    if err then
3,018✔
147
      return nil, err, gettime() - start_time
3,012✔
148
    end
149
  end
150

151
  -- it's ours to have
152
  self.owner = co
1,584✔
153
  self.call_count = 1
1,584✔
154
  return start_time and (gettime() - start_time) or 0
1,584✔
155
end
156

157

158
--- Releases the lock currently held.
159
-- Releasing a lock that is not owned by the current co-routine will return
160
-- an error.
161
-- returns true, or nil+err on an error
162
function lock:release()
12✔
163
  local co = coroutine_running()
1,536✔
164

165
  if co ~= self.owner then
1,536✔
166
    return nil, "cannot release a lock not owned"
6✔
167
  end
168

169
  self.call_count = self.call_count - 1
1,530✔
170
  if self.call_count > 0 then
1,530✔
171
    -- same coro is still holding it
172
    return true
×
173
  end
174

175
  -- need a loop, since individual coroutines might have been removed
176
  -- (or canceled externally, eg. via `copas.removethread`/`future:cancel`)
177
  -- so there might be holes, or stale entries whose wakeup will fail
178
  while self.q_tip < self.q_tail do
3,042✔
179
    local next_up = self.queue[self.q_tip]
1,524✔
180
    self.queue[self.q_tip] = nil
1,524✔
181
    self.q_tip = self.q_tip + 1
1,524✔
182
    if next_up and copas.wakeup(next_up) then
1,527✔
183
      self.owner = next_up
12✔
184
      return true
12✔
185
    end
186
    -- either an empty hole, or 'next_up' was canceled and will never resume
187
    -- to release the lock itself, so move on to the next waiter in line
188
  end
189
  -- queue is empty, reset pointers
190
  self.owner = nil
1,518✔
191
  self.q_tip = 0
1,518✔
192
  self.q_tail = 0
1,518✔
193
  return true
1,518✔
194
end
195

196

197

198
return lock
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc