Compare commits

..
2 Commits
Author SHA1 Message Date
Jakob Borg c7f1d854bd wip
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-19 14:30:42 +02:00
Jakob Borg f877dfed4e wip
Signed-off-by: Jakob Borg <jakob@kastelo.net>
2026-05-19 13:09:29 +02:00
26 changed files with 174 additions and 408 deletions
@@ -22,7 +22,6 @@ jobs:
if: github.repository_owner == 'syncthing' if: github.repository_owner == 'syncthing'
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false
matrix: matrix:
pkg: pkg:
- stcrashreceiver - stcrashreceiver
+2 -2
View File
@@ -164,7 +164,7 @@ jobs:
needs: needs:
- build-test - build-test
- package-linux - package-linux
- package-illumos # - package-illumos
- package-cross - package-cross
- package-source - package-source
- package-debian - package-debian
@@ -646,7 +646,7 @@ jobs:
needs: needs:
- codesign-windows - codesign-windows
- package-linux - package-linux
- package-illumos # - package-illumos
- package-macos - package-macos
- package-cross - package-cross
- package-source - package-source
@@ -1,76 +0,0 @@
2026-05-21 10:03:01 INF syncthing v2.1.1-dev.9.gb3b7d228.dirty-morecrashrep "Hafnium Hornet" (go1.26.3 darwin-arm64) jb@jbo-m3wl72rv 2026-05-21 07:58:11 UTC [stnoupgrade] (log.pkg=main)
2026-05-21 10:03:01 INF No automatic upgrades; STNOUPGRADE environment variable defined (log.pkg=main)
2026-05-21 10:03:01 INF Calculated our device ID (device=I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU log.pkg=syncthing)
2026-05-21 10:03:01 INF Overall rate limit in use (send="is unlimited" recv="is unlimited" log.pkg=connections)
2026-05-21 10:03:01 INF Using discovery mechanism (identity="IPv4 local broadcast discovery on port 21027" log.pkg=discover)
2026-05-21 10:03:01 INF Using discovery mechanism (identity="IPv6 local multicast discovery on address [ff12::8384]:21027" log.pkg=discover)
2026-05-21 10:03:01 INF TCP listener starting (address=127.0.0.1:22001 log.pkg=connections)
2026-05-21 10:03:01 INF Ready to synchronize (folder.id=default folder.type=sendreceive log.pkg=model)
2026-05-21 10:03:01 INF QUIC listener starting (address=127.0.0.1:22001 log.pkg=connections)
2026-05-21 10:03:01 INF GUI and API listening (address=127.0.0.1:8081 log.pkg=api)
...
2026-05-21 10:03:01 INF Access the GUI via the following URL: http://127.0.0.1:8081/ (log.pkg=api)
2026-05-21 10:03:01 INF Loaded configuration (name=s1 log.pkg=syncthing)
2026-05-21 10:03:01 INF Loaded peer device configuration (device=MRIW7OK name=s2 address="[tcp://127.0.0.1:22002 quic://127.0.0.1:22002]" log.pkg=syncthing)
2026-05-21 10:03:01 INF Completed initial scan (folder.id=default folder.type=sendreceive log.pkg=model)
0xee9de1fe260
2026-05-21 10:03:02 INF Measured hashing performance (perf="2789.71 MB/s" log.pkg=syncthing)
Panic at 2026-05-21T10:03:02+02:00
runtime: marked free object in span 0x108b34d20, elemsize=8 freeindex=34 (bad use of unsafe.Pointer or having race conditions? try -d=checkptr or -race)
0xee9de1fe000 alloc marked
0xee9de1fe008 alloc marked
...
0xee9de1fe250 free unmarked
0xee9de1fe258 free unmarked
0xee9de1fe260 free marked zombie
7 6 5 4 3 2 1 0 f e d c b a 9 8 0123456789abcdef
00000ee9de1fe260: 00000000 00000000 ........
0xee9de1fe268 free unmarked
0xee9de1fe270 free unmarked
...
0xee9de1fff60 free unmarked
0xee9de1fff68 free unmarked
0xee9de1fff70 free unmarked
0xee9de1fff78 free unmarked
fatal error: found pointer to free object
runtime stack:
runtime.throw({0x105881781?, 0x8?})
runtime/panic.go:1229 +0x38 fp=0x16bf82bb0 sp=0x16bf82b80 pc=0x104f0ca48
runtime.(*mspan).reportZombies(0x108b34d20)
runtime/mgcsweep.go:893 +0x314 fp=0x16bf82c30 sp=0x16bf82bb0 pc=0x104ec10b4
runtime.(*sweepLocked).sweep(0x16bf82d88?, 0x0)
runtime/mgcsweep.go:673 +0xbd0 fp=0x16bf82d50 sp=0x16bf82c30 pc=0x104ec0840
runtime.(*mcentral).uncacheSpan(0x16bf82db8?, 0x104ea4954?)
runtime/mcentral.go:237 +0xbc fp=0x16bf82d80 sp=0x16bf82d50 pc=0x104eaac3c
runtime.(*mcache).releaseAll(0x1089a85f0)
runtime/mcache.go:322 +0x188 fp=0x16bf82df0 sp=0x16bf82d80 pc=0x104eaa4e8
runtime.(*mcache).prepareForSweep(0x1089a85f0)
runtime/mcache.go:366 +0x4c fp=0x16bf82e20 sp=0x16bf82df0 pc=0x104eaa61c
runtime.gcMarkTermination.func4(0xee9de005808)
runtime/mgc.go:1546 +0x24 fp=0x16bf82e50 sp=0x16bf82e20 pc=0x104f076e4
runtime.forEachPInternal(0x10656f798)
runtime/proc.go:2167 +0x178 fp=0x16bf82ee0 sp=0x16bf82e50 pc=0x104eda728
runtime.gcMarkTermination.forEachP.func7()
runtime/proc.go:2126 +0x40 fp=0x16bf82f10 sp=0x16bf82ee0 pc=0x104eb3130
runtime.systemstack(0x7fc000)
runtime/asm_arm64.s:399 +0x68 fp=0x16bf82f20 sp=0x16bf82f10 pc=0x104f12888
goroutine 84 gp=0xee9de45c1e0 m=3 mp=0xee9de019008 [flushing proc caches]:
runtime.systemstack_switch()
runtime/asm_arm64.s:347 +0x8 fp=0xee9de805c40 sp=0xee9de805c30 pc=0x104f12808
runtime.forEachP(...)
runtime/proc.go:2112
runtime.gcMarkTermination({0xc0?, 0x1331f928480ca?, 0xc?, 0x0?})
runtime/mgc.go:1545 +0x5f4 fp=0xee9de805e80 sp=0xee9de805c40 pc=0x104eb28c4
runtime.gcMarkDone()
runtime/mgc.go:1173 +0x364 fp=0xee9de805f20 sp=0xee9de805e80 pc=0x104eb1bc4
runtime.gcBgMarkWorker(0xee9de341810)
runtime/mgc.go:1912 +0x29c fp=0xee9de805fb0 sp=0xee9de805f20 pc=0x104eb372c
runtime.gcBgMarkStartWorkers.gowrap1()
runtime/mgc.go:1695 +0x20 fp=0xee9de805fd0 sp=0xee9de805fb0 pc=0x104eb3470
runtime.goexit({})
runtime/asm_arm64.s:1447 +0x4 fp=0xee9de805fd0 sp=0xee9de805fd0 pc=0x104f14a04
created by runtime.gcBgMarkStartWorkers in goroutine 1
runtime/mgc.go:1695 +0x134
+4 -14
View File
@@ -136,25 +136,15 @@ func (d *diskStore) Exists(path string) bool {
} }
func (d *diskStore) clean() { func (d *diskStore) clean() {
numDeleted := 0 for len(d.currentFiles) > 0 && (len(d.currentFiles) > d.maxFiles || d.currentSize > d.maxBytes) {
for idx := range d.currentFiles { f := d.currentFiles[0]
if len(d.currentFiles)-numDeleted < d.maxFiles && d.currentSize < d.maxBytes {
break
}
f := d.currentFiles[idx]
log.Println("Removing", f.path) log.Println("Removing", f.path)
if err := os.Remove(f.path); err != nil { if err := os.Remove(f.path); err != nil {
log.Println("Failed to remove file:", err) log.Println("Failed to remove file:", err)
} }
d.currentFiles = d.currentFiles[1:]
d.currentSize -= f.size d.currentSize -= f.size
numDeleted = idx + 1
} }
// Compact currentFiles
copy(d.currentFiles, d.currentFiles[numDeleted:])
d.currentFiles = d.currentFiles[:len(d.currentFiles)-numDeleted]
var oldest time.Duration var oldest time.Duration
if len(d.currentFiles) > 0 { if len(d.currentFiles) > 0 {
oldest = time.Since(time.Unix(d.currentFiles[0].mtime, 0)).Truncate(time.Minute) oldest = time.Since(time.Unix(d.currentFiles[0].mtime, 0)).Truncate(time.Minute)
@@ -168,7 +158,7 @@ func (d *diskStore) clean() {
} }
func (d *diskStore) inventory() error { func (d *diskStore) inventory() error {
d.currentFiles = d.currentFiles[:0] d.currentFiles = nil
d.currentSize = 0 d.currentSize = 0
err := filepath.Walk(d.dir, func(path string, info os.FileInfo, err error) error { err := filepath.Walk(d.dir, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
+8 -11
View File
@@ -20,7 +20,6 @@ import (
"io" "io"
"log" "log"
"net/http" "net/http"
"net/http/pprof"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@@ -30,7 +29,7 @@ import (
raven "github.com/getsentry/raven-go" raven "github.com/getsentry/raven-go"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/ur/contract" "github.com/syncthing/syncthing/lib/ur"
) )
const maxRequestSize = 1 << 20 // 1 MiB const maxRequestSize = 1 << 20 // 1 MiB
@@ -90,7 +89,6 @@ func main() {
if params.MetricsListen != "" { if params.MetricsListen != "" {
mmux := http.NewServeMux() mmux := http.NewServeMux()
mmux.Handle("/metrics", promhttp.Handler()) mmux.Handle("/metrics", promhttp.Handler())
mmux.HandleFunc("/debug/pprof/", pprof.Index)
go func() { go func() {
if err := http.ListenAndServe(params.MetricsListen, mmux); err != nil { if err := http.ListenAndServe(params.MetricsListen, mmux); err != nil {
log.Fatalln("HTTP serve metrics:", err) log.Fatalln("HTTP serve metrics:", err)
@@ -125,13 +123,12 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http
return return
} }
if pat, ok := ignore.match(bs); ok { if ignore.match(bs) {
metricIgnoreMatchesTotal.WithLabelValues(pat).Inc()
result = "ignored" result = "ignored"
return return
} }
var reports []contract.FailureReport var reports []ur.FailureReport
err = json.Unmarshal(bs, &reports) err = json.Unmarshal(bs, &reports)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
@@ -179,7 +176,7 @@ func handleFailureFn(dsn, failureDir string, ignore *ignorePatterns) func(w http
} }
} }
func saveFailureWithGoroutines(data contract.FailureData, failureDir string) (string, error) { func saveFailureWithGoroutines(data ur.FailureData, failureDir string) (string, error) {
bs := make([]byte, len(data.Description)+len(data.Goroutines)) bs := make([]byte, len(data.Description)+len(data.Goroutines))
copy(bs, data.Description) copy(bs, data.Description)
copy(bs[len(data.Description):], data.Goroutines) copy(bs[len(data.Description):], data.Goroutines)
@@ -219,14 +216,14 @@ func loadIgnorePatterns(path string) (*ignorePatterns, error) {
return &ignorePatterns{patterns: patterns}, nil return &ignorePatterns{patterns: patterns}, nil
} }
func (i *ignorePatterns) match(report []byte) (string, bool) { func (i *ignorePatterns) match(report []byte) bool {
if i == nil { if i == nil {
return "", false return false
} }
for _, re := range i.patterns { for _, re := range i.patterns {
if re.Match(report) { if re.Match(report) {
return re.String(), true return true
} }
} }
return "", false return false
} }
-20
View File
@@ -37,24 +37,4 @@ var (
Subsystem: "crashreceiver", Subsystem: "crashreceiver",
Name: "diskstore_oldest_age_seconds", Name: "diskstore_oldest_age_seconds",
}) })
metricSentryReportsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "crashreceiver",
Name: "sentry_reports_total",
}, []string{"result"})
metricIgnoreMatchesTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "crashreceiver",
Name: "ignore_matches_total",
}, []string{"pattern"})
metricSourceCodeLoadsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "syncthing",
Subsystem: "crashreceiver",
Name: "source_code_loads_total",
}, []string{"result"})
metricSourceCodeCacheSize = promauto.NewGauge(prometheus.GaugeOpts{
Namespace: "syncthing",
Subsystem: "crashreceiver",
Name: "source_code_cache_size",
})
) )
+8 -8
View File
@@ -10,7 +10,6 @@ import (
"bytes" "bytes"
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"log" "log"
"regexp" "regexp"
@@ -53,15 +52,11 @@ func (s *sentryService) Serve(ctx context.Context) {
pkt, err := parseCrashReport(req.reportID, req.data) pkt, err := parseCrashReport(req.reportID, req.data)
if err != nil { if err != nil {
log.Println("Failed to parse crash report:", err) log.Println("Failed to parse crash report:", err)
metricSentryReportsTotal.WithLabelValues("parse_failure").Inc()
continue continue
} }
if err := sendReport(s.dsn, pkt, req.userID); err != nil { if err := sendReport(s.dsn, pkt, req.userID); err != nil {
log.Println("Failed to send crash report:", err) log.Println("Failed to send crash report:", err)
metricSentryReportsTotal.WithLabelValues("send_failure").Inc()
continue
} }
metricSentryReportsTotal.WithLabelValues("success").Inc()
case <-ctx.Done(): case <-ctx.Done():
return return
@@ -74,7 +69,6 @@ func (s *sentryService) Send(reportID, userID string, data []byte) bool {
case s.inbox <- sentryRequest{reportID, userID, data}: case s.inbox <- sentryRequest{reportID, userID, data}:
return true return true
default: default:
metricCrashReportsTotal.WithLabelValues("overflow").Inc()
return false return false
} }
} }
@@ -114,10 +108,11 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
version, err := build.ParseVersion(string(parts[0])) version, err := build.ParseVersion(string(parts[0]))
if err != nil { if err != nil {
return nil, fmt.Errorf("%w in %q", err, parts[0]) return nil, err
} }
report = parts[1] report = parts[1]
foundPanic := false
var subjectLine []byte var subjectLine []byte
for { for {
parts = bytes.SplitN(report, []byte("\n"), 2) parts = bytes.SplitN(report, []byte("\n"), 2)
@@ -128,9 +123,14 @@ func parseCrashReport(path string, report []byte) (*raven.Packet, error) {
line := parts[0] line := parts[0]
report = parts[1] report = parts[1]
if bytes.HasPrefix(line, []byte("panic:")) || bytes.HasPrefix(line, []byte("fatal error:")) { if foundPanic {
// The previous line was our "Panic at ..." header. We are now
// at the beginning of the real panic trace and this is our
// subject line.
subjectLine = line subjectLine = line
break break
} else if bytes.HasPrefix(line, []byte("Panic at")) {
foundPanic = true
} }
} }
+11 -18
View File
@@ -9,33 +9,26 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"testing" "testing"
) )
func TestParseReport(t *testing.T) { func TestParseReport(t *testing.T) {
files, err := filepath.Glob("_testdata/*.log") bs, err := os.ReadFile("_testdata/panic.log")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
for _, file := range files {
bs, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
pkt, err := parseCrashReport("1/2/345", bs) pkt, err := parseCrashReport("1/2/345", bs)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
}
bs, err = pkt.JSON()
if err != nil {
t.Fatal(err)
}
fmt.Printf("%s\n", bs)
} }
bs, err = pkt.JSON()
if err != nil {
t.Fatal(err)
}
fmt.Printf("%s\n", bs)
} }
func TestCrashReportFingerprint(t *testing.T) { func TestCrashReportFingerprint(t *testing.T) {
+11 -26
View File
@@ -15,33 +15,23 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
lru "github.com/hashicorp/golang-lru/v2"
) )
const ( const (
urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/" urlPrefix = "https://raw.githubusercontent.com/syncthing/syncthing/"
httpTimeout = 10 * time.Second httpTimeout = 10 * time.Second
maxCacheEntries = 1000
) )
type cacheKey struct {
version string
file string
}
type githubSourceCodeLoader struct { type githubSourceCodeLoader struct {
mut sync.Mutex mut sync.Mutex
version string version string
cache map[string]map[string][][]byte // version -> file -> lines
cache *lru.TwoQueueCache[cacheKey, [][]byte] // version & file -> lines client *http.Client
client *http.Client
} }
func newGithubSourceCodeLoader() *githubSourceCodeLoader { func newGithubSourceCodeLoader() *githubSourceCodeLoader {
cache, _ := lru.New2Q[cacheKey, [][]byte](maxCacheEntries)
return &githubSourceCodeLoader{ return &githubSourceCodeLoader{
cache: cache, cache: make(map[string]map[string][][]byte),
client: &http.Client{Timeout: httpTimeout}, client: &http.Client{Timeout: httpTimeout},
} }
} }
@@ -49,6 +39,9 @@ func newGithubSourceCodeLoader() *githubSourceCodeLoader {
func (l *githubSourceCodeLoader) LockWithVersion(version string) { func (l *githubSourceCodeLoader) LockWithVersion(version string) {
l.mut.Lock() l.mut.Lock()
l.version = version l.version = version
if _, ok := l.cache[version]; !ok {
l.cache[version] = make(map[string][][]byte)
}
} }
func (l *githubSourceCodeLoader) Unlock() { func (l *githubSourceCodeLoader) Unlock() {
@@ -57,13 +50,11 @@ func (l *githubSourceCodeLoader) Unlock() {
func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]byte, int) { func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]byte, int) {
filename = filepath.ToSlash(filename) filename = filepath.ToSlash(filename)
key := cacheKey{version: l.version, file: filename} lines, ok := l.cache[l.version][filename]
lines, ok := l.cache.Get(key)
if !ok { if !ok {
// Cache whatever we managed to find (or nil if nothing, so we don't try again) // Cache whatever we managed to find (or nil if nothing, so we don't try again)
defer func() { defer func() {
l.cache.Add(key, lines) l.cache[l.version][filename] = lines
metricSourceCodeCacheSize.Set(float64(l.cache.Len()))
}() }()
knownPrefixes := []string{"/lib/", "/cmd/"} knownPrefixes := []string{"/lib/", "/cmd/"}
@@ -82,25 +73,19 @@ func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]b
resp, err := l.client.Get(url) resp, err := l.client.Get(url)
if err != nil { if err != nil {
fmt.Println("Loading source:", err) fmt.Println("Loading source:", err)
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
return nil, 0 return nil, 0
} }
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
fmt.Println("Loading source:", resp.Status) fmt.Println("Loading source:", resp.Status)
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
return nil, 0 return nil, 0
} }
data, err := io.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if err != nil { if err != nil {
fmt.Println("Loading source:", err.Error()) fmt.Println("Loading source:", err.Error())
metricSourceCodeLoadsTotal.WithLabelValues("failed").Inc()
return nil, 0 return nil, 0
} }
lines = bytes.Split(data, []byte{'\n'}) lines = bytes.Split(data, []byte{'\n'})
metricSourceCodeLoadsTotal.WithLabelValues("loaded").Inc()
} else {
metricSourceCodeLoadsTotal.WithLabelValues("cached").Inc()
} }
return getLineFromLines(lines, line, context) return getLineFromLines(lines, line, context)
+29 -10
View File
@@ -7,18 +7,21 @@
package main package main
import ( import (
"bytes"
"io" "io"
"log" "log"
"net/http" "net/http"
"path" "path"
"strings" "strings"
"sync"
) )
type crashReceiver struct { type crashReceiver struct {
store *diskStore store *diskStore
sentry *sentryService sentry *sentryService
ignore *ignorePatterns ignore *ignorePatterns
ignoredMut sync.RWMutex
ignored map[string]struct{}
} }
func (r *crashReceiver) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (r *crashReceiver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
@@ -66,6 +69,12 @@ func (r *crashReceiver) serveGet(reportID string, w http.ResponseWriter, _ *http
// serveHead responds to HEAD requests by checking if the named report // serveHead responds to HEAD requests by checking if the named report
// already exists in the system. // already exists in the system.
func (r *crashReceiver) serveHead(reportID string, w http.ResponseWriter, _ *http.Request) { func (r *crashReceiver) serveHead(reportID string, w http.ResponseWriter, _ *http.Request) {
r.ignoredMut.RLock()
_, ignored := r.ignored[reportID]
r.ignoredMut.RUnlock()
if ignored {
return // found
}
if !r.store.Exists(reportID) { if !r.store.Exists(reportID) {
http.Error(w, "Not found", http.StatusNotFound) http.Error(w, "Not found", http.StatusNotFound)
} }
@@ -78,7 +87,17 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
metricCrashReportsTotal.WithLabelValues(result).Inc() metricCrashReportsTotal.WithLabelValues(result).Inc()
}() }()
r.ignoredMut.RLock()
_, ignored := r.ignored[reportID]
r.ignoredMut.RUnlock()
if ignored {
result = "ignored_cached"
io.Copy(io.Discard, req.Body)
return // found
}
// Read at most maxRequestSize of report data. // Read at most maxRequestSize of report data.
log.Println("Receiving report", reportID)
lr := io.LimitReader(req.Body, maxRequestSize) lr := io.LimitReader(req.Body, maxRequestSize)
bs, err := io.ReadAll(lr) bs, err := io.ReadAll(lr)
if err != nil { if err != nil {
@@ -87,12 +106,14 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
return return
} }
first := string(bytes.TrimSpace(bytes.Split(bs, []byte("\n"))[0])) if r.ignore.match(bs) {
r.ignoredMut.Lock()
if pat, ok := r.ignore.match(bs); ok { if r.ignored == nil {
metricIgnoreMatchesTotal.WithLabelValues(pat).Inc() r.ignored = make(map[string]struct{})
}
r.ignored[reportID] = struct{}{}
r.ignoredMut.Unlock()
result = "ignored" result = "ignored"
log.Printf("Ignored report %s, matched: %s (%s)", reportID[:8], pat, first)
return return
} }
@@ -100,15 +121,13 @@ func (r *crashReceiver) servePut(reportID string, w http.ResponseWriter, req *ht
// Store the report // Store the report
if !r.store.Put(reportID, bs) { if !r.store.Put(reportID, bs) {
log.Println("Failed to store report (queue full):", reportID[:8]) log.Println("Failed to store report (queue full):", reportID)
result = "queue_failure" result = "queue_failure"
} }
// Send the report to Sentry // Send the report to Sentry
if !r.sentry.Send(reportID, userIDFor(req), bs) { if !r.sentry.Send(reportID, userIDFor(req), bs) {
log.Println("Failed to send report to sentry (queue full):", reportID[:8]) log.Println("Failed to send report to sentry (queue full):", reportID)
result = "sentry_failure" result = "sentry_failure"
} }
log.Printf("Received report %s (%s)", reportID[:8], first)
} }
+4 -4
View File
@@ -15,7 +15,7 @@ import (
"io" "io"
"log/slog" "log/slog"
"os" "os"
"path/filepath" "path"
"runtime" "runtime"
"slices" "slices"
"strings" "strings"
@@ -77,7 +77,7 @@ func newInMemoryStore(dir string, flushInterval time.Duration, blobs blob.Store)
slog.Error("Failed to find database in blob storage", "error", cerr) slog.Error("Failed to find database in blob storage", "error", cerr)
return s return s
} }
fd, cerr := os.Create(filepath.Join(s.dir, "records.db")) fd, cerr := os.Create(path.Join(s.dir, "records.db"))
if cerr != nil { if cerr != nil {
slog.Error("Failed to create database file", "error", cerr) slog.Error("Failed to create database file", "error", cerr)
return s return s
@@ -257,7 +257,7 @@ func (s *inMemoryStore) write() (err error) {
} }
}() }()
dbf := filepath.Join(s.dir, "records.db") dbf := path.Join(s.dir, "records.db")
fd, err := os.Create(dbf + ".tmp") fd, err := os.Create(dbf + ".tmp")
if err != nil { if err != nil {
return err return err
@@ -340,7 +340,7 @@ func (s *inMemoryStore) write() (err error) {
} }
func (s *inMemoryStore) read() (int, error) { func (s *inMemoryStore) read() (int, error) {
fd, err := os.Open(filepath.Join(s.dir, "records.db")) fd, err := os.Open(path.Join(s.dir, "records.db"))
if err != nil { if err != nil {
return 0, err return 0, err
} }
+8 -27
View File
@@ -7,8 +7,6 @@
package main package main
import ( import (
"net/http"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
) )
@@ -115,11 +113,14 @@ var (
) )
const ( const (
dbOpGet = "get" dbOpGet = "get"
dbOpPut = "put" dbOpPut = "put"
dbOpMerge = "merge" dbOpMerge = "merge"
dbResSuccess = "success" dbOpDelete = "delete"
dbResNotFound = "not_found" dbResSuccess = "success"
dbResNotFound = "not_found"
dbResError = "error"
dbResUnmarshalError = "unmarsh_err"
) )
func init() { func init() {
@@ -131,24 +132,4 @@ func init() {
databaseOperations, databaseOperationSeconds, databaseOperations, databaseOperationSeconds,
databaseWriteSeconds, databaseLastWritten, databaseWriteSeconds, databaseLastWritten,
retryAfterLevel) retryAfterLevel)
// Prewarm important counters so they're available with zero values at
// startup
apiRequestsTotal.WithLabelValues(http.MethodGet, "200")
apiRequestsTotal.WithLabelValues(http.MethodGet, "404")
apiRequestsTotal.WithLabelValues(http.MethodPost, "204")
apiRequestsTotal.WithLabelValues(http.MethodPost, "400")
apiRequestsTotal.WithLabelValues(http.MethodPost, "403")
lookupRequestsTotal.WithLabelValues("success")
lookupRequestsTotal.WithLabelValues("not_found_ever")
lookupRequestsTotal.WithLabelValues("not_found_recent")
announceRequestsTotal.WithLabelValues("success")
announceRequestsTotal.WithLabelValues("bad_request")
announceRequestsTotal.WithLabelValues("no_certificate")
replicationSendsTotal.WithLabelValues("success")
replicationRecvsTotal.WithLabelValues("success")
} }
+3 -7
View File
@@ -915,14 +915,10 @@ func (u upgradeCmd) Run() error {
case err != nil && !os.IsNotExist(err): case err != nil && !os.IsNotExist(err):
slog.Error("Failed to lock for upgrade", slogutil.Error(err)) slog.Error("Failed to lock for upgrade", slogutil.Error(err))
os.Exit(1) os.Exit(1)
case locked || os.IsNotExist(err): case locked:
// We got the lock, or the config directory didn't exist, so we
// can do a direct upgrade
err = upgrade.To(release)
default:
// We didn't get the lock, because Syncthing was running, so
// upgrade via REST.
err = upgradeViaRest() err = upgradeViaRest()
default:
err = upgrade.To(release)
} }
} }
if err != nil { if err != nil {
+1 -1
View File
@@ -233,7 +233,7 @@ func copyStderr(stderr io.Reader, dst io.Writer) {
dst.Write([]byte(line)) dst.Write([]byte(line))
if panicFd == nil && (strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:") || strings.HasPrefix(line, "runtime:")) { if panicFd == nil && (strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:")) {
panicFd, err = os.Create(locations.GetTimestamped(locations.PanicLog)) panicFd, err = os.Create(locations.GetTimestamped(locations.PanicLog))
if err != nil { if err != nil {
slog.Error("Failed to create panic log", slogutil.Error(err)) slog.Error("Failed to create panic log", slogutil.Error(err))
+5 -3
View File
@@ -42,8 +42,8 @@ require (
github.com/wlynxg/anet v0.0.5 github.com/wlynxg/anet v0.0.5
golang.org/x/crypto v0.51.0 golang.org/x/crypto v0.51.0
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a
golang.org/x/net v0.55.0 golang.org/x/net v0.54.0
golang.org/x/sys v0.45.0 golang.org/x/sys v0.44.0
golang.org/x/text v0.37.0 golang.org/x/text v0.37.0
golang.org/x/time v0.15.0 golang.org/x/time v0.15.0
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
@@ -111,7 +111,9 @@ replace github.com/gobwas/glob v0.2.3 => github.com/calmh/glob v0.0.0-2022061508
replace github.com/jackpal/gateway v1.1.1 => github.com/marbens-arch/gateway v1.1.2-0.20260308173556-c567cc04e7d4 replace github.com/jackpal/gateway v1.1.1 => github.com/marbens-arch/gateway v1.1.2-0.20260308173556-c567cc04e7d4
// https://github.com/mattn/go-sqlite3/pull/1338 // https://github.com/mattn/go-sqlite3/pull/1338
replace github.com/mattn/go-sqlite3 v1.14.44 => github.com/calmh/go-sqlite3 v1.14.35-0.20260509063420-822b4765116d // https://github.com/mattn/go-sqlite3/pull/1399
// https://github.com/mattn/go-sqlite3/pull/1400
replace github.com/mattn/go-sqlite3 v1.14.44 => github.com/calmh/go-sqlite3 v1.14.45-0.20260519121030-00c8bf368e65
tool ( tool (
github.com/calmh/xdr/cmd/genxdr github.com/calmh/xdr/cmd/genxdr
+6 -6
View File
@@ -19,8 +19,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/calmh/glob v0.0.0-20220615080505-1d823af5017b h1:Fjm4GuJ+TGMgqfGHN42IQArJb77CfD/mAwLbDUoJe6g= github.com/calmh/glob v0.0.0-20220615080505-1d823af5017b h1:Fjm4GuJ+TGMgqfGHN42IQArJb77CfD/mAwLbDUoJe6g=
github.com/calmh/glob v0.0.0-20220615080505-1d823af5017b/go.mod h1:91K7jfEsgJSyfSrX+gmrRfZMtntx6JsHolWubGXDopg= github.com/calmh/glob v0.0.0-20220615080505-1d823af5017b/go.mod h1:91K7jfEsgJSyfSrX+gmrRfZMtntx6JsHolWubGXDopg=
github.com/calmh/go-sqlite3 v1.14.35-0.20260509063420-822b4765116d h1:liVtRMlDBqDyR4qKkMRIFTdUK6+aoG6Oh97DKnhrCHc= github.com/calmh/go-sqlite3 v1.14.45-0.20260519121030-00c8bf368e65 h1:4tbv1D+AkdxV4si6cdT3+8lr/c/gJqBdW6yGi8I7z3E=
github.com/calmh/go-sqlite3 v1.14.35-0.20260509063420-822b4765116d/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= github.com/calmh/go-sqlite3 v1.14.45-0.20260519121030-00c8bf368e65/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/calmh/incontainer v1.0.0 h1:g2cTUtZuFGmMGX8GoykPkN1Judj2uw8/3/aEtq4Z/rg= github.com/calmh/incontainer v1.0.0 h1:g2cTUtZuFGmMGX8GoykPkN1Judj2uw8/3/aEtq4Z/rg=
github.com/calmh/incontainer v1.0.0/go.mod h1:eOhqnw15c9X+4RNBe0W3HlUZFfX16O0EDsCOInTndHY= github.com/calmh/incontainer v1.0.0/go.mod h1:eOhqnw15c9X+4RNBe0W3HlUZFfX16O0EDsCOInTndHY=
github.com/calmh/xdr v1.2.0 h1:GaGSNH4ZDw9kNdYqle6+RcAENiaQ8/611Ok+jQbBEeU= github.com/calmh/xdr v1.2.0 h1:GaGSNH4ZDw9kNdYqle6+RcAENiaQ8/611Ok+jQbBEeU=
@@ -280,8 +280,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -309,8 +309,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 h1:HjU6IWBiAgRIdAJ9/y1rwCn+UELEmwV+VsTLzj/W4sE= golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6 h1:HjU6IWBiAgRIdAJ9/y1rwCn+UELEmwV+VsTLzj/W4sE=
golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE= golang.org/x/telemetry v0.0.0-20260508192327-42602be52be6/go.mod h1:Eqhaxk/wZsWEH8CRxLwj6xzEJbz7k1EFGqx7nyCoabE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+2 -1
View File
@@ -13,6 +13,7 @@ import (
"errors" "errors"
"iter" "iter"
"os" "os"
"path"
"path/filepath" "path/filepath"
"sync" "sync"
"testing" "testing"
@@ -1167,7 +1168,7 @@ func TestOpenSpecialName(t *testing.T) {
// Create a "base" dir that is in the way if the path becomes // Create a "base" dir that is in the way if the path becomes
// incorrectly truncated in the next steps. // incorrectly truncated in the next steps.
base := filepath.Join(dir, "test") base := path.Join(dir, "test")
if err := os.Mkdir(base, 0o755); err != nil { if err := os.Mkdir(base, 0o755); err != nil {
t.Fatal(err) t.Fatal(err)
} }
+1 -1
View File
@@ -16,7 +16,7 @@ import (
// or, somewhere along the way the "+" in the version tag disappeared: // or, somewhere along the way the "+" in the version tag disappeared:
// syncthing v1.23.7-dev.26.gdf7b56ae.dirty-stversionextra "Fermium Flea" (go1.20.5 darwin-arm64) jb@ok.kastelo.net 2023-07-12 06:55:26 UTC [Some Wrapper, purego, stnoupgrade] // syncthing v1.23.7-dev.26.gdf7b56ae.dirty-stversionextra "Fermium Flea" (go1.20.5 darwin-arm64) jb@ok.kastelo.net 2023-07-12 06:55:26 UTC [Some Wrapper, purego, stnoupgrade]
var ( var (
longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)[^\[]*(?:\[(.+)\])?`) longVersionRE = regexp.MustCompile(`syncthing\s+(v[^\s]+)\s+"([^"]+)"\s\(([^\s]+)\s+([^-]+)-([^)]+)\)\s+([^\s]+)[^\[]*(?:\[(.+)\])?$`)
gitExtraRE = regexp.MustCompile(`\.\d+\.g[0-9a-f]+`) // ".1.g6aaae618" gitExtraRE = regexp.MustCompile(`\.\d+\.g[0-9a-f]+`) // ".1.g6aaae618"
gitExtraSepRE = regexp.MustCompile(`[.-]`) // dot or dash gitExtraSepRE = regexp.MustCompile(`[.-]`) // dot or dash
) )
-14
View File
@@ -57,20 +57,6 @@ func TestParseVersion(t *testing.T) {
Extra: []string{"Some Wrapper", "purego", "stnoupgrade"}, Extra: []string{"Some Wrapper", "purego", "stnoupgrade"},
}, },
}, },
{
longVersion: `2026-05-18 14:53:32 INF syncthing v2.0.3 "Hafnium Hornet" (go1.25.0 darwin-amd64) builder@github.syncthing.net 2025-08-22 07:00:05 UTC [stnoupgrade] (log.pkg=main)`,
parsed: VersionParts{
Version: "v2.0.3",
Tag: "v2.0.3",
Commit: "",
Codename: "Hafnium Hornet",
Runtime: "go1.25.0",
GOOS: "darwin",
GOARCH: "amd64",
Builder: "builder@github.syncthing.net",
Extra: []string{"stnoupgrade"},
},
},
} }
for _, tc := range cases { for _, tc := range cases {
+3 -5
View File
@@ -13,7 +13,6 @@ import (
"log/slog" "log/slog"
"net" "net"
"net/url" "net/url"
"slices"
"sync" "sync"
"time" "time"
@@ -186,10 +185,9 @@ func (t *tcpListener) WANAddresses() []*url.URL {
t.mut.RUnlock() t.mut.RUnlock()
// If we support ReusePort, and we are already announcing an unspecified // If we support ReusePort, add an unspecified zero port address, which will be resolved by the discovery server
// address, add an unspecified zero port address, which will be resolved // in hopes that TCP punch through works.
// by the discovery server in hopes that TCP punch through works. if dialer.SupportsReusePort {
if dialer.SupportsReusePort && slices.ContainsFunc(uris, func(u *url.URL) bool { return u.Hostname() == "0.0.0.0" }) {
uri := *t.uri uri := *t.uri
uri.Host = "0.0.0.0:0" uri.Host = "0.0.0.0:0"
uris = append([]*url.URL{&uri}, uris...) uris = append([]*url.URL{&uri}, uris...)
+1
View File
@@ -289,6 +289,7 @@ func (c *globalClient) sendAnnouncement(ctx context.Context, timer *time.Timer)
resp.Body.Close() resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode > 299 { if resp.StatusCode < 200 || resp.StatusCode > 299 {
slog.DebugContext(ctx, "announce POST", "server", c.server, "status", resp.Status)
c.setError(errors.New(resp.Status)) c.setError(errors.New(resp.Status))
if h := resp.Header.Get("Retry-After"); h != "" { if h := resp.Header.Get("Retry-After"); h != "" {
+2 -2
View File
@@ -20,7 +20,7 @@ import (
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/svcutil"
"github.com/syncthing/syncthing/lib/ur/contract" "github.com/syncthing/syncthing/lib/ur"
) )
type indexHandler struct { type indexHandler struct {
@@ -470,7 +470,7 @@ func (s *indexHandler) logSequenceAnomaly(msg string, extra map[string]any) {
extraStrs[k] = fmt.Sprint(v) extraStrs[k] = fmt.Sprint(v)
} }
s.evLogger.Log(events.Failure, contract.FailureData{ s.evLogger.Log(events.Failure, ur.FailureData{
Description: msg, Description: msg,
Extra: extraStrs, Extra: extraStrs,
}) })
-13
View File
@@ -282,16 +282,3 @@ func clear(v interface{}, since int) error {
} }
return nil return nil
} }
type FailureReport struct {
FailureData
Count int
Version string
}
type FailureData struct {
Description string
Goroutines string
Extra map[string]string
}
+26 -14
View File
@@ -23,7 +23,6 @@ import (
"github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/svcutil" "github.com/syncthing/syncthing/lib/svcutil"
"github.com/syncthing/syncthing/lib/tlsutil" "github.com/syncthing/syncthing/lib/tlsutil"
"github.com/syncthing/syncthing/lib/ur/contract"
"github.com/thejerf/suture/v4" "github.com/thejerf/suture/v4"
) )
@@ -40,10 +39,23 @@ var (
invalidEventDataType = "failure event data is not a string" invalidEventDataType = "failure event data is not a string"
) )
func FailureDataWithGoroutines(description string) contract.FailureData { type FailureReport struct {
FailureData
Count int
Version string
}
type FailureData struct {
Description string
Goroutines string
Extra map[string]string
}
func FailureDataWithGoroutines(description string) FailureData {
var buf strings.Builder var buf strings.Builder
pprof.Lookup("goroutine").WriteTo(&buf, 1) pprof.Lookup("goroutine").WriteTo(&buf, 1)
return contract.FailureData{ return FailureData{
Description: description, Description: description,
Goroutines: buf.String(), Goroutines: buf.String(),
Extra: make(map[string]string), Extra: make(map[string]string),
@@ -74,7 +86,7 @@ type failureHandler struct {
type failureStat struct { type failureStat struct {
first, last time.Time first, last time.Time
count int count int
data contract.FailureData data FailureData
} }
func (h *failureHandler) Serve(ctx context.Context) error { func (h *failureHandler) Serve(ctx context.Context) error {
@@ -93,24 +105,24 @@ func (h *failureHandler) Serve(ctx context.Context) error {
if !ok { if !ok {
// Just to be safe - shouldn't ever happen, as // Just to be safe - shouldn't ever happen, as
// evChan is set to nil when unsubscribing. // evChan is set to nil when unsubscribing.
h.addReport(contract.FailureData{Description: evChanClosed}, time.Now()) h.addReport(FailureData{Description: evChanClosed}, time.Now())
evChan = nil evChan = nil
continue continue
} }
var data contract.FailureData var data FailureData
switch d := e.Data.(type) { switch d := e.Data.(type) {
case string: case string:
data.Description = d data.Description = d
case contract.FailureData: case FailureData:
data = d data = d
default: default:
// Same here, shouldn't ever happen. // Same here, shouldn't ever happen.
h.addReport(contract.FailureData{Description: invalidEventDataType}, time.Now()) h.addReport(FailureData{Description: invalidEventDataType}, time.Now())
continue continue
} }
h.addReport(data, e.Time) h.addReport(data, e.Time)
case <-timer.C: case <-timer.C:
reports := make([]contract.FailureReport, 0, len(h.buf)) reports := make([]FailureReport, 0, len(h.buf))
now := time.Now() now := time.Now()
for descr, stat := range h.buf { for descr, stat := range h.buf {
if now.Sub(stat.last) > minDelay || now.Sub(stat.first) > maxDelay { if now.Sub(stat.last) > minDelay || now.Sub(stat.first) > maxDelay {
@@ -140,7 +152,7 @@ func (h *failureHandler) Serve(ctx context.Context) error {
if sub != nil { if sub != nil {
sub.Unsubscribe() sub.Unsubscribe()
if len(h.buf) > 0 { if len(h.buf) > 0 {
reports := make([]contract.FailureReport, 0, len(h.buf)) reports := make([]FailureReport, 0, len(h.buf))
for _, stat := range h.buf { for _, stat := range h.buf {
reports = append(reports, newFailureReport(stat)) reports = append(reports, newFailureReport(stat))
} }
@@ -167,7 +179,7 @@ func (h *failureHandler) applyOpts(opts config.OptionsConfiguration, sub events.
return url, nil, nil return url, nil, nil
} }
func (h *failureHandler) addReport(data contract.FailureData, evTime time.Time) { func (h *failureHandler) addReport(data FailureData, evTime time.Time) {
if stat, ok := h.buf[data.Description]; ok { if stat, ok := h.buf[data.Description]; ok {
stat.last = evTime stat.last = evTime
stat.count++ stat.count++
@@ -192,7 +204,7 @@ func (*failureHandler) String() string {
return "FailureHandler" return "FailureHandler"
} }
func sendFailureReports(ctx context.Context, reports []contract.FailureReport, url string) { func sendFailureReports(ctx context.Context, reports []FailureReport, url string) {
var b bytes.Buffer var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(reports); err != nil { if err := json.NewEncoder(&b).Encode(reports); err != nil {
panic(err) panic(err)
@@ -223,8 +235,8 @@ func sendFailureReports(ctx context.Context, reports []contract.FailureReport, u
resp.Body.Close() resp.Body.Close()
} }
func newFailureReport(stat *failureStat) contract.FailureReport { func newFailureReport(stat *failureStat) FailureReport {
return contract.FailureReport{ return FailureReport{
FailureData: stat.data, FailureData: stat.data,
Count: stat.count, Count: stat.count,
Version: build.LongVersion, Version: build.LongVersion,
-55
View File
@@ -15,7 +15,6 @@ import (
"github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/build"
"github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/fs"
) )
func TestTaggedFilename(t *testing.T) { func TestTaggedFilename(t *testing.T) {
@@ -257,57 +256,3 @@ func TestArchiveFoldersCreationPermission(t *testing.T) {
t.Errorf("földer2 permissions %v, want %v", folder2VersionsInfo.Mode(), folder2Perms) t.Errorf("földer2 permissions %v, want %v", folder2VersionsInfo.Mode(), folder2Perms)
} }
} }
func TestDupDirTreeWritePermissions(t *testing.T) {
// The structure should be replicated, with user permission bits set along the way
srcFs := fs.NewFilesystem(fs.FilesystemTypeFake, "TestDupDirTreeWritePermissions/srcFs")
dstFs := fs.NewFilesystem(fs.FilesystemTypeFake, "TestDupDirTreeWritePermissions/dstFs")
// A source dir to duplicate
_ = srcFs.Mkdir("foo", 0o444)
_ = srcFs.Mkdir("foo/bar", 0o555)
_ = srcFs.Mkdir("foo/bar/baz", 0o000)
// Duplication should succeed
if err := dupDirTree(srcFs, dstFs, "foo/bar/baz"); err != nil {
t.Fatal(err)
}
// Permissions should be the same, but with read/write/execute bits for
// the user
if info, err := dstFs.Lstat("foo"); err != nil || info.Mode() != 0o744 {
t.Fatalf("foo: 0o%o", info.Mode())
}
if info, err := dstFs.Lstat("foo/bar"); err != nil || info.Mode() != 0o755 {
t.Fatalf("foo/bar: 0o%o", info.Mode())
}
if info, err := dstFs.Lstat("foo/bar/baz"); err != nil || info.Mode() != 0o700 {
t.Fatalf("foo/bar/baz: 0o%o", info.Mode())
}
}
func TestDupDirFastPath(t *testing.T) {
srcFs := fs.NewFilesystem(fs.FilesystemTypeFake, "TestDupDirFastPath/srcFs")
dstFs := fs.NewFilesystem(fs.FilesystemTypeFake, "TestDupDirFastPath/dstFs")
// A source dir to duplicate
_ = srcFs.Mkdir("foo", 0o444)
_ = srcFs.Mkdir("foo/bar", 0o555)
_ = srcFs.Mkdir("foo/bar/baz", 0o000)
// The destination exists, but with too few permission bits
_ = dstFs.MkdirAll("foo/bar/baz", 0o555)
// Duplication should succeed
if err := dupDirTree(srcFs, dstFs, "foo/bar/baz"); err != nil {
t.Fatal(err)
}
// Permissions for the destination should have been updated. (This
// differs from what would have been created by the duplication of the
// 0o000 dir in the src, because it already existed.)
if info, err := dstFs.Lstat("foo/bar/baz"); err != nil || info.Mode() != 0o755 {
t.Fatalf("foo/bar/baz: 0o%o", info.Mode())
}
}
+39 -69
View File
@@ -192,78 +192,48 @@ func archiveFile(method fs.CopyRangeMethod, srcFs, dstFs fs.Filesystem, filePath
return err return err
} }
// dupDirTree ensures folderPath exists in dstFs, copying permissions mostly func dupDirTree(srcFs, dstFs fs.Filesystem, folderPath string) error {
// from srcFs. Permissions are altered to have the user read, write, and // Return early if the folder already exists.
// execute bits set so that Syncthing file operations are possible within _, err := dstFs.Stat(folderPath)
// the destination directory. if err == nil || !fs.IsNotExist(err) {
//
// We want to retain the source group and other bits so that we do not
// inadvertently open up a directory for users who shouldn't have access to
// it, but we do not consider it a security issue to open up the permissions
// for the current user.
//
// This is based on os.MkdirAll with our srcFs adjustments.
func dupDirTree(srcFs, dstFs fs.Filesystem, path string) error {
const (
allPerms = 0o777
minDirPerms = 0o700
)
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
if dir, err := dstFs.Lstat(path); err == nil {
if !dir.IsDir() {
return errors.New("destination exists but is not a directory")
}
if dir.Mode()&minDirPerms != minDirPerms {
// We want all the required permission bits set
_ = dstFs.Chmod(path, dir.Mode()&allPerms|minDirPerms)
}
return nil
}
// Slow path: make sure parent exists and then call Mkdir for path.
// Extract the parent folder from path by first removing any trailing
// path separator and then scanning backward until finding a path
// separator or reaching the beginning of the string.
i := len(path) - 1
for i >= 0 && os.IsPathSeparator(path[i]) {
i--
}
for i >= 0 && !os.IsPathSeparator(path[i]) {
i--
}
if i < 0 {
i = 0
}
// If there is a parent directory, and it is not the volume name,
// recurse to ensure parent directory exists.
if parent := path[:i]; len(parent) > len(filepath.VolumeName(path)) {
if err := dupDirTree(srcFs, dstFs, parent); err != nil {
return err
}
}
// Parent now exists; invoke Mkdir and use its result.
srcPerms := fs.FileMode(minDirPerms)
if srcDir, err := srcFs.Lstat(path); err == nil {
srcPerms = srcDir.Mode()&allPerms | minDirPerms
}
if err := dstFs.Mkdir(path, srcPerms); err != nil {
// Handle arguments like "foo/." by
// double-checking that directory doesn't exist.
dir, err1 := dstFs.Lstat(path)
if err1 == nil && dir.IsDir() {
return nil
}
return err return err
} }
hadParent := true
for i := range folderPath {
if os.IsPathSeparator(folderPath[i]) {
// If the parent folder didn't exist, then this folder doesn't exist
// so we can skip the check
if hadParent {
_, err := dstFs.Stat(folderPath[:i])
if err == nil {
continue
}
if !fs.IsNotExist(err) {
return err
}
}
hadParent = false
err := dupDirWithPerms(srcFs, dstFs, folderPath[:i])
if err != nil {
return err
}
}
}
return dupDirWithPerms(srcFs, dstFs, folderPath)
}
// Extra chmod to ensure our permissions override umask func dupDirWithPerms(srcFs, dstFs fs.Filesystem, folderPath string) error {
_ = dstFs.Chmod(path, srcPerms) srcStat, err := srcFs.Stat(folderPath)
if err != nil {
return nil return err
}
// If we call Mkdir with srcStat.Mode(), we won't get the expected perms because of umask
// So, we create the folder with 0700, and then change the perms to the srcStat.Mode()
err = dstFs.Mkdir(folderPath, 0o700)
if err != nil {
return err
}
return dstFs.Chmod(folderPath, srcStat.Mode())
} }
func restoreFile(method fs.CopyRangeMethod, src, dst fs.Filesystem, filePath string, versionTime time.Time, tagger fileTagger) error { func restoreFile(method fs.CopyRangeMethod, src, dst fs.Filesystem, filePath string, versionTime time.Time, tagger fileTagger) error {