Skip to content

Commit f09d97f

Browse files
committed
fix orphaned subprocess leak on service Re registration and toctou
1 parent 63d46fa commit f09d97f

2 files changed

Lines changed: 360 additions & 7 deletions

File tree

internal/node/service_registry.go

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ type ServiceRegistry struct {
7272
services map[string]Service
7373
dht dhtProvider
7474

75+
// names claimed by a Register that has not committed yet, guarded by mu
76+
pending map[string]*pendingRegistration
77+
78+
// bumped by TeardownAll so a Register still running cannot commit into the
79+
// map it just emptied, guarded by mu
80+
generation uint64
81+
7582
// reprovideNow asks the node to run a reprovide cycle now, so a service
7683
// registered after the loop last ran does not wait a whole interval to be
7784
// advertised. Optional; nil outside a running node.
@@ -83,6 +90,12 @@ type ServiceRegistry struct {
8390
backendProbeTimeout time.Duration
8491
}
8592

93+
// claim on a name held by a Register that has not committed yet, set when the
94+
// name is unregistered before the registration commits
95+
type pendingRegistration struct {
96+
cancelled bool
97+
}
98+
8699
// NewServiceRegistry constructs a registry bounding backend probes by
87100
// backendProbeTimeout. A zero or negative value falls back to
88101
// defaultDHTProbeTimeout, so callers can pass an unset
@@ -94,6 +107,7 @@ func NewServiceRegistry(d dhtProvider, backendProbeTimeout time.Duration) *Servi
94107
}
95108
return &ServiceRegistry{
96109
services: map[string]Service{},
110+
pending: map[string]*pendingRegistration{},
97111
dht: d,
98112
backendProbeTimeout: backendProbeTimeout,
99113
}
@@ -106,6 +120,8 @@ func NewServiceRegistry(d dhtProvider, backendProbeTimeout time.Duration) *Servi
106120
// A backend that does not answer is registered but not advertised, rather than
107121
// rejected: backends routinely start after the node does, and the reprovide
108122
// loop picks them up once they answer.
123+
//
124+
// registering a name again replaces the instance and tears the old one down
109125
func (r *ServiceRegistry) Register(ctx context.Context, svc Service) error {
110126
info := svc.Info()
111127
if info.Type == api.ServiceType_SERVICE_TYPE_UNSPECIFIED {
@@ -123,26 +139,53 @@ func (r *ServiceRegistry) Register(ctx context.Context, svc Service) error {
123139
return fmt.Errorf("invalid service name %q: %w", info.Name, err)
124140
}
125141

126-
// Policy is evaluated on type://name and the registry is keyed on name,
127-
// so a second type under the same name would let a grant for one reach
128-
// the other. One name, one service.
129-
r.mu.RLock()
130-
existing, taken := r.services[info.Name]
131-
r.mu.RUnlock()
132-
if taken && existing.Info().Type != info.Type {
142+
// the registry is keyed on name but policy is evaluated on type://name, so
143+
// one name maps to one type. reserve under the same lock as the check, or
144+
// two concurrent registrations both pass it and disagree about the owner
145+
r.mu.Lock()
146+
if r.pending == nil {
147+
r.pending = map[string]*pendingRegistration{}
148+
}
149+
if existing, taken := r.services[info.Name]; taken && existing.Info().Type != info.Type {
150+
r.mu.Unlock()
133151
return fmt.Errorf("service name %q is already registered as %s; a name cannot serve two types", info.Name, existing.Info().Type)
134152
}
153+
if _, inFlight := r.pending[info.Name]; inFlight {
154+
r.mu.Unlock()
155+
return fmt.Errorf("service name %q is already being registered", info.Name)
156+
}
157+
reservation := &pendingRegistration{}
158+
r.pending[info.Name] = reservation
159+
generation := r.generation
160+
r.mu.Unlock()
161+
162+
// release drops the reservation, teardown is for aborts after Init where
163+
// the service has already built something to clean up
164+
release := func(teardown bool) {
165+
r.mu.Lock()
166+
delete(r.pending, info.Name)
167+
r.mu.Unlock()
168+
if !teardown {
169+
return
170+
}
171+
if err := svc.Teardown(); err != nil {
172+
logger.Errorf("[ServiceRegistry] Teardown %s after abandoned registration: %v", info.Name, err)
173+
}
174+
}
135175

136176
if err := svc.Init(ctx); err != nil {
177+
release(false)
137178
return fmt.Errorf("init %s: %w", info.Name, err)
138179
}
139180

140181
srvNameCID, err := serviceNameToCID(info.Type, info.Name)
141182
if err != nil {
183+
release(true)
142184
return err
143185
}
144186
srvTypeCID, err := serviceTypeToCID(info.Type)
145187
if err != nil {
188+
release(true)
146189
return err
147190
}
148191

@@ -163,10 +206,33 @@ func (r *ServiceRegistry) Register(ctx context.Context, svc Service) error {
163206
}
164207
}
165208

209+
// the registry may have been torn down or the name unregistered while we
210+
// were initialising, so don't leave the service in the map
166211
r.mu.Lock()
212+
if r.generation != generation {
213+
r.mu.Unlock()
214+
release(true)
215+
return fmt.Errorf("registry was torn down while %q was being registered", info.Name)
216+
}
217+
if reservation.cancelled {
218+
r.mu.Unlock()
219+
release(true)
220+
return fmt.Errorf("service name %q was unregistered while it was being registered", info.Name)
221+
}
222+
delete(r.pending, info.Name)
223+
replaced := r.services[info.Name]
167224
r.services[info.Name] = svc
168225
r.mu.Unlock()
169226

227+
// teardown outside the lock, it kills a subprocess
228+
if replaced != nil && replaced != svc {
229+
if err := replaced.Teardown(); err != nil {
230+
logger.Errorf("[ServiceRegistry] Teardown replaced %s: %v", info.Name, err)
231+
} else {
232+
logger.Infof("[ServiceRegistry] Replaced %s/%s, previous instance torn down", info.Type, info.Name)
233+
}
234+
}
235+
170236
if probeErr == nil {
171237
logger.Infof("[ServiceRegistry] Registered %s/%s (name CID: %s, type CID: %s)", info.Type, info.Name, srvNameCID, srvTypeCID)
172238
} else if r.reprovideNow != nil {
@@ -181,6 +247,11 @@ func (r *ServiceRegistry) Unregister(ctx context.Context, name string) error {
181247
r.mu.Lock()
182248
svc, ok := r.services[name]
183249
delete(r.services, name)
250+
// cancel a registration of this name that has not committed, else it lands
251+
// after we return and the name comes back
252+
if p, inFlight := r.pending[name]; inFlight {
253+
p.cancelled = true
254+
}
184255
r.mu.Unlock()
185256
if !ok {
186257
return nil
@@ -238,10 +309,14 @@ func (r *ServiceRegistry) insertService(svc Service) {
238309

239310
// TeardownAll calls Teardown on every registered service and clears the
240311
// map. Per-service errors are logged; iteration continues.
312+
//
313+
// the generation bump voids registrations still running, which would otherwise
314+
// commit into the map this just emptied
241315
func (r *ServiceRegistry) TeardownAll() {
242316
r.mu.Lock()
243317
svcs := r.services
244318
r.services = map[string]Service{}
319+
r.generation++
245320
r.mu.Unlock()
246321
for name, svc := range svcs {
247322
if err := svc.Teardown(); err != nil {

0 commit comments

Comments
 (0)