fix(xray): synchronize the process version and apiPort fields

Start writes p.version and p.apiPort (via refreshVersion/refreshAPIPort) after
flipping the process to running, while GetXrayVersion and GetAPIPort read them
lock-free from the status and traffic poll goroutines. The struct mutex
deliberately excluded these fields, so a restart racing a poll was a real data
race — a torn read of the version string header can crash. Extend the mutex to
cover version and apiPort, doing the blocking version probe before taking the
lock.
This commit is contained in:
MHSanaei
2026-07-15 03:33:38 +02:00
parent 40da7fdb76
commit f2f11bc042
2 changed files with 68 additions and 15 deletions
+22 -15
View File
@@ -126,11 +126,12 @@ func NewTestProcess(xrayConfig *Config, configPath string) *Process {
} }
type process struct { type process struct {
// mu guards the process lifecycle fields (cmd, done, exitErr) which are // mu guards the process lifecycle fields (cmd, done, exitErr) plus version and
// written by Start/startCommand and the waitForCommand goroutine while being // apiPort, which are written by Start/startCommand/refreshVersion/refreshAPIPort
// read concurrently by IsRunning/GetErr/GetResult/Stop from other goroutines // while being read concurrently by IsRunning/GetErr/GetResult/GetXrayVersion/
// (status endpoint, check-xray-running job). Snapshot under the lock, then do // GetAPIPort/Stop from other goroutines (status endpoint, check-xray-running
// any blocking syscall (Wait/Signal/Kill) on the local copy without holding it. // and traffic jobs). Snapshot under the lock, then do any blocking syscall
// (Wait/Signal/Kill) on the local copy without holding it.
mu sync.RWMutex mu sync.RWMutex
cmd *exec.Cmd cmd *exec.Cmd
done chan struct{} done chan struct{}
@@ -281,11 +282,15 @@ func (p *process) GetResult() string {
// GetXrayVersion returns the version string of the Xray process. // GetXrayVersion returns the version string of the Xray process.
func (p *process) GetXrayVersion() string { func (p *process) GetXrayVersion() string {
p.mu.RLock()
defer p.mu.RUnlock()
return p.version return p.version
} }
// GetAPIPort returns the API port used by the Xray process. // GetAPIPort returns the API port used by the Xray process.
func (p *Process) GetAPIPort() int { func (p *Process) GetAPIPort() int {
p.mu.RLock()
defer p.mu.RUnlock()
return p.apiPort return p.apiPort
} }
@@ -474,28 +479,30 @@ func (p *Process) GetUptime() uint64 {
// refreshAPIPort updates the API port from the inbound configs. // refreshAPIPort updates the API port from the inbound configs.
func (p *process) refreshAPIPort() { func (p *process) refreshAPIPort() {
port := 0
for _, inbound := range p.config.InboundConfigs { for _, inbound := range p.config.InboundConfigs {
if inbound.Tag == "api" { if inbound.Tag == "api" {
p.apiPort = inbound.Port port = inbound.Port
break break
} }
} }
p.mu.Lock()
p.apiPort = port
p.mu.Unlock()
} }
// refreshVersion updates the version string by running the Xray binary with -version. // refreshVersion updates the version string by running the Xray binary with -version.
func (p *process) refreshVersion() { func (p *process) refreshVersion() {
version := "Unknown"
cmd := exec.CommandContext(context.Background(), GetBinaryPath(), "-version") cmd := exec.CommandContext(context.Background(), GetBinaryPath(), "-version")
data, err := cmd.Output() if data, err := cmd.Output(); err == nil {
if err != nil { if datas := bytes.Split(data, []byte(" ")); len(datas) > 1 {
p.version = "Unknown" version = string(datas[1])
} else {
datas := bytes.Split(data, []byte(" "))
if len(datas) <= 1 {
p.version = "Unknown"
} else {
p.version = string(datas[1])
} }
} }
p.mu.Lock()
p.version = version
p.mu.Unlock()
} }
// Start launches the Xray process with the current configuration. // Start launches the Xray process with the current configuration.
+46
View File
@@ -54,3 +54,49 @@ func TestProcessLifecycleFieldsRaceSafe(t *testing.T) {
close(stop) close(stop)
wg.Wait() wg.Wait()
} }
// TestProcessVersionAPIPortRaceSafe writes version/apiPort the way Start's
// refresh helpers do while GetXrayVersion/GetAPIPort read them concurrently.
// Run with -race: an unsynchronized access to either field is reported.
func TestProcessVersionAPIPortRaceSafe(t *testing.T) {
inner := &process{
logWriter: NewLogWriter(),
config: &Config{InboundConfigs: []InboundConfig{{Tag: "api", Port: 12345}}},
}
p := &Process{inner}
var wg sync.WaitGroup
stop := make(chan struct{})
wg.Go(func() {
for {
select {
case <-stop:
return
default:
}
p.refreshAPIPort()
inner.mu.Lock()
inner.version = "v1.2.3"
inner.mu.Unlock()
}
})
for range 4 {
wg.Go(func() {
for {
select {
case <-stop:
return
default:
}
_ = p.GetAPIPort()
_ = p.GetXrayVersion()
}
})
}
time.Sleep(50 * time.Millisecond)
close(stop)
wg.Wait()
}