Backend audit fixes (upstream-file edits wrapped in // lx: markers): - experimental/libbox oom_report.go/report.go: OOM reports + configuration.json (server secrets/keys) were written world-writable — 0o777 dirs / 0o666 files → 0o700 / 0o600. [sec-perms] - daemon/server.go + experimental/libbox/command_server.go: gRPC auth secret compared with != (timing oracle) → crypto/subtle.ConstantTimeCompare. [sec-consttime] - service/oomkiller/timer.go: network-extension cleanupTriggered logic was inverted, so FreeOSMemory was never called after a trigger; flip both assignments so a trigger schedules the deferred free and the next poll runs + clears it. [sec-oomcleanup] - transport/v2rayxhttp/client.go (lx-native file): session id used math/rand → crypto/rand, matching Xray's uuid.New() entropy and removing the spoof surface. - daemon/started_service_tailscale_ssh.go: forwardSSHAgentChannel leaked a goroutine + the ssh-agent fd on every closed session (second io.Copy blocked on an idle agent Read forever); tie both copies + the session ctx to a cancel that closes both ends. [sec-sshagent] - daemon/managed_service.go: TriggerOOMReport had no gate — rate-limit to 1/min so an authenticated client can't spin secret-bearing dumps. [sec-oomgate] - route/reachability_lx.go (lx idle-suspend file): idle tick read r.idleStop in select while stopIdleSuspend niled it after close (race + goroutine leak on Close-during-tick); pass the stop channel to the loop by value. go build ./... (default) and the D9 shaterd linux build (tags with_quic,with_wireguard,with_utls,badlinkname,tfogo_checklinkname0,with_xhttp, with_awg,with_lx_command) are green; go vet clean (2 pre-existing unsafe.Pointer warnings in TriggerDebugCrash/debug.go, untouched); go test ./route/... ./daemon/... ./service/oomkiller/... green incl. -race with with_lx_idle_suspend and v2rayxhttp with with_xhttp. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
103 lines
2.6 KiB
Go
103 lines
2.6 KiB
Go
//go:build darwin || linux || windows
|
|
|
|
package libbox
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"time"
|
|
|
|
C "github.com/sagernet/sing-box/constant"
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
)
|
|
|
|
type reportMetadata struct {
|
|
Source string `json:"source,omitempty"`
|
|
BundleIdentifier string `json:"bundleIdentifier,omitempty"`
|
|
ProcessName string `json:"processName,omitempty"`
|
|
ProcessPath string `json:"processPath,omitempty"`
|
|
StartedAt string `json:"startedAt,omitempty"`
|
|
AppVersion string `json:"appVersion,omitempty"`
|
|
AppMarketingVersion string `json:"appMarketingVersion,omitempty"`
|
|
CoreVersion string `json:"coreVersion,omitempty"`
|
|
GoVersion string `json:"goVersion,omitempty"`
|
|
}
|
|
|
|
func baseReportMetadata() reportMetadata {
|
|
processPath, _ := os.Executable()
|
|
processName := filepath.Base(processPath)
|
|
if processName == "." {
|
|
processName = ""
|
|
}
|
|
return reportMetadata{
|
|
Source: sCrashReportSource,
|
|
ProcessName: processName,
|
|
ProcessPath: processPath,
|
|
CoreVersion: C.Version,
|
|
GoVersion: GoVersion(),
|
|
}
|
|
}
|
|
|
|
func writeReportFile(destPath string, name string, content []byte) {
|
|
filePath := filepath.Join(destPath, name)
|
|
// lx:begin sec-perms
|
|
// Report files may carry the config snapshot (secrets) — owner-only.
|
|
os.WriteFile(filePath, content, 0o600)
|
|
// lx:end sec-perms
|
|
chownReport(filePath)
|
|
}
|
|
|
|
func writeReportMetadata(destPath string, metadata any) {
|
|
data, err := json.Marshal(metadata)
|
|
if err != nil {
|
|
return
|
|
}
|
|
writeReportFile(destPath, "metadata.json", data)
|
|
}
|
|
|
|
func copyConfigSnapshot(destPath string) {
|
|
snapshotPath := configSnapshotPath()
|
|
content, err := os.ReadFile(snapshotPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(bytes.TrimSpace(content)) == 0 {
|
|
return
|
|
}
|
|
writeReportFile(destPath, "configuration.json", content)
|
|
}
|
|
|
|
func initReportDir(path string) {
|
|
// lx:begin sec-perms
|
|
os.MkdirAll(path, 0o700)
|
|
// lx:end sec-perms
|
|
chownReport(path)
|
|
}
|
|
|
|
func chownReport(path string) {
|
|
if runtime.GOOS != "android" && runtime.GOOS != "windows" {
|
|
os.Chown(path, sUserID, sGroupID)
|
|
}
|
|
}
|
|
|
|
func nextAvailableReportPath(reportsDir string, timestamp time.Time) (string, error) {
|
|
destName := timestamp.Format("2006-01-02T15-04-05")
|
|
destPath := filepath.Join(reportsDir, destName)
|
|
_, err := os.Stat(destPath)
|
|
if os.IsNotExist(err) {
|
|
return destPath, nil
|
|
}
|
|
for i := 1; i <= 1000; i++ {
|
|
suffixedPath := filepath.Join(reportsDir, destName+"-"+strconv.Itoa(i))
|
|
_, err = os.Stat(suffixedPath)
|
|
if os.IsNotExist(err) {
|
|
return suffixedPath, nil
|
|
}
|
|
}
|
|
return "", E.New("no available report path for ", destName)
|
|
}
|