Skip to content

Commit 7599f16

Browse files
committed
More timeouts and bounding
1 parent 5be1e12 commit 7599f16

9 files changed

Lines changed: 461 additions & 47 deletions

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
watch (inotify on Linux, kqueue on macOS, `ReadDirectoryChangesW` on
1515
Windows) with a bounded stat-poll fallback. Clipboard writes are now atomic
1616
and serialized with an OS file lock, so concurrent instances can no longer
17-
lose or corrupt clipboard entries.
17+
lose or corrupt clipboard entries. Lock waits time out and clipboard reads
18+
are capped in size and time, so a stuck or hostile process sharing the
19+
clipboard file cannot hang or bloat other instances.
1820

1921
### Added
2022

mshell/DirWatch.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ func (fm *FileManager) clipboardWatchLoop() {
7070
}
7171
defer watcher.Close()
7272

73+
// The watch only now exists: anything written before this point (startup,
74+
// or the dormant gap before a restart) produced no event, so reconcile
75+
// with the actual on-disk state once. Every later write is covered by an
76+
// event because the watch was established before this refresh.
77+
fm.applyClipboardChange(false)
78+
7379
for {
7480
select {
7581
case <-fm.watchDone:

mshell/DirWatch_linux.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ func (w *dirWatcher) readLoop() {
6666
return
6767
}
6868
nameLen := int(event.Len)
69+
if offset+unix.SizeofInotifyEvent+nameLen > n {
70+
// A record claiming to extend past the read is malformed;
71+
// the kernel never splits events, so stop parsing rather
72+
// than slice past the data (which would panic the process).
73+
break
74+
}
6975
name := ""
7076
if nameLen > 0 {
7177
nameBytes := buf[offset+unix.SizeofInotifyEvent : offset+unix.SizeofInotifyEvent+nameLen]

mshell/DirWatch_windows.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,16 +134,18 @@ func (w *dirWatcher) readLoop() {
134134
// NextEntryOffset uint32, Action uint32, FileNameLength uint32 (bytes),
135135
// FileName []uint16 (not NUL-terminated).
136136
func (w *dirWatcher) parseAndSend(buf []byte) {
137-
offset := uint32(0)
137+
// All arithmetic is in int (64-bit) so a malformed length or chain
138+
// offset can never wrap and loop forever or slice out of range.
139+
offset := 0
138140
for {
139-
if int(offset)+12 > len(buf) {
141+
if offset+12 > len(buf) {
140142
return
141143
}
142-
next := binary.LittleEndian.Uint32(buf[offset:])
143-
nameLen := binary.LittleEndian.Uint32(buf[offset+8:])
144+
next := int(binary.LittleEndian.Uint32(buf[offset:]))
145+
nameLen := int(binary.LittleEndian.Uint32(buf[offset+8:]))
144146
nameStart := offset + 12
145147
nameEnd := nameStart + nameLen
146-
if int(nameEnd) > len(buf) {
148+
if nameEnd > len(buf) {
147149
return
148150
}
149151
nameBytes := buf[nameStart:nameEnd]
@@ -155,6 +157,11 @@ func (w *dirWatcher) parseAndSend(buf []byte) {
155157
if next == 0 {
156158
return
157159
}
160+
if next < 12 || offset+next > len(buf) {
161+
// A chain offset that is too small or points past the buffer is
162+
// malformed; stop parsing rather than spin on it.
163+
return
164+
}
158165
offset += next
159166
}
160167
}

mshell/FileLock_unix.go

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,53 @@
33
package main
44

55
import (
6+
"fmt"
67
"os"
8+
"path/filepath"
9+
"time"
710

811
"golang.org/x/sys/unix"
912
)
1013

14+
const fileLockRetryInterval = 50 * time.Millisecond
15+
16+
// openNonBlock is ORed into opens of the shared clipboard file so that a FIFO
17+
// planted at that path cannot block open(2) forever waiting for a writer. It
18+
// has no effect on regular files.
19+
const openNonBlock = unix.O_NONBLOCK
20+
1121
// fileLock is an exclusive advisory lock backed by flock(2). The lock is tied
1222
// to the open file description, so the OS releases it automatically if the
1323
// process dies while holding it; no stale-lock cleanup is ever needed.
1424
type fileLock struct {
1525
f *os.File
1626
}
1727

18-
// acquireFileLock blocks until an exclusive lock on path is acquired,
19-
// creating the lock file if needed.
20-
func acquireFileLock(path string) (*fileLock, error) {
28+
// acquireFileLock acquires an exclusive lock on path, creating the lock file
29+
// if needed. The wait is bounded: it retries a non-blocking lock until
30+
// timeout, then fails, so a stuck or hostile lock holder can never hang the
31+
// caller indefinitely.
32+
func acquireFileLock(path string, timeout time.Duration) (*fileLock, error) {
2133
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644)
2234
if err != nil {
2335
return nil, err
2436
}
37+
deadline := time.Now().Add(timeout)
2538
for {
26-
err = unix.Flock(int(f.Fd()), unix.LOCK_EX)
27-
if err != unix.EINTR {
28-
break
39+
err = unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB)
40+
if err == nil {
41+
return &fileLock{f: f}, nil
2942
}
43+
if err != unix.EWOULDBLOCK && err != unix.EAGAIN && err != unix.EINTR {
44+
f.Close()
45+
return nil, err
46+
}
47+
if time.Now().After(deadline) {
48+
f.Close()
49+
return nil, fmt.Errorf("timed out waiting for lock on %s", filepath.Base(path))
50+
}
51+
time.Sleep(fileLockRetryInterval)
3052
}
31-
if err != nil {
32-
f.Close()
33-
return nil, err
34-
}
35-
return &fileLock{f: f}, nil
3653
}
3754

3855
func (l *fileLock) release() {

mshell/FileLock_windows.go

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,57 @@
33
package main
44

55
import (
6+
"fmt"
67
"os"
8+
"path/filepath"
9+
"time"
710

811
"golang.org/x/sys/windows"
912
)
1013

14+
const fileLockRetryInterval = 50 * time.Millisecond
15+
16+
// openNonBlock mirrors the unix constant ORed into clipboard opens. Windows
17+
// has no FIFO-in-the-filesystem equivalent whose open blocks, so it is a
18+
// no-op here.
19+
const openNonBlock = 0
20+
1121
// fileLock is an exclusive lock backed by LockFileEx. The lock is tied to the
1222
// open handle, so the OS releases it automatically if the process dies while
1323
// holding it; no stale-lock cleanup is ever needed.
1424
type fileLock struct {
1525
f *os.File
1626
}
1727

18-
// acquireFileLock blocks until an exclusive lock on path is acquired,
19-
// creating the lock file if needed.
20-
func acquireFileLock(path string) (*fileLock, error) {
28+
// acquireFileLock acquires an exclusive lock on path, creating the lock file
29+
// if needed. The wait is bounded: it retries a non-blocking lock until
30+
// timeout, then fails, so a stuck or hostile lock holder can never hang the
31+
// caller indefinitely.
32+
func acquireFileLock(path string, timeout time.Duration) (*fileLock, error) {
2133
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0644)
2234
if err != nil {
2335
return nil, err
2436
}
25-
// Lock one byte at offset 0 (the offset lives in the Overlapped struct).
26-
// Without LOCKFILE_FAIL_IMMEDIATELY this blocks until the lock is free.
27-
ol := new(windows.Overlapped)
28-
err = windows.LockFileEx(windows.Handle(f.Fd()), windows.LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, ol)
29-
if err != nil {
30-
f.Close()
31-
return nil, err
37+
deadline := time.Now().Add(timeout)
38+
for {
39+
// Lock one byte at offset 0 (the offset lives in the Overlapped
40+
// struct). LOCKFILE_FAIL_IMMEDIATELY keeps each attempt non-blocking.
41+
ol := new(windows.Overlapped)
42+
err = windows.LockFileEx(windows.Handle(f.Fd()),
43+
windows.LOCKFILE_EXCLUSIVE_LOCK|windows.LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, ol)
44+
if err == nil {
45+
return &fileLock{f: f}, nil
46+
}
47+
if err != windows.ERROR_LOCK_VIOLATION {
48+
f.Close()
49+
return nil, err
50+
}
51+
if time.Now().After(deadline) {
52+
f.Close()
53+
return nil, fmt.Errorf("timed out waiting for lock on %s", filepath.Base(path))
54+
}
55+
time.Sleep(fileLockRetryInterval)
3256
}
33-
return &fileLock{f: f}, nil
3457
}
3558

3659
func (l *fileLock) release() {

0 commit comments

Comments
 (0)