logind: drop the seat from the GC queue after draining its devices

manager_gc() pops a seat off m->seat_gc_queue and clears
seat->in_gc_queue before calling seat_free(). seat_free() then drains
the seat's devices, and device_detach() calls seat_add_to_gc_queue() for
a seat that just lost its last master device, re-prepending the dying
seat onto m->seat_gc_queue. seat_free() removed itself from the queue up
front, before that drain, so the re-queued seat is left on the queue as a
dangling pointer once mfree() runs.

Follow-up for 718d006a63
This commit is contained in:
Luca Boccassi
2026-07-03 19:35:55 +01:00
parent a8b429387a
commit 166d46eb2e
2 changed files with 44 additions and 3 deletions

View File

@@ -73,9 +73,6 @@ Seat* seat_free(Seat *s) {
if (!s)
return NULL;
if (s->in_gc_queue)
LIST_REMOVE(gc_queue, s->manager->seat_gc_queue, s);
while (s->sessions)
session_free(s->sessions);
@@ -84,6 +81,13 @@ Seat* seat_free(Seat *s) {
while (s->devices)
device_free(s->devices);
/* Draining sessions and devices above can put us back onto the GC queue: device_detach()
* re-queues the seat once it loses its last master device. Remove ourselves from the queue
* only here, after everything that could re-add us has run, otherwise we leave a dangling
* pointer behind for the next manager_gc() pass. */
if (s->in_gc_queue)
LIST_REMOVE(gc_queue, s->manager->seat_gc_queue, s);
hashmap_remove(s->manager->seats, s->id);
set_free(s->uevents);

View File

@@ -8,6 +8,8 @@
#include "logind.h"
#include "logind-action.h"
#include "logind-button.h"
#include "logind-device.h"
#include "logind-seat.h"
#include "logind-session.h"
#include "logind-user.h"
#include "sleep-config.h"
@@ -69,6 +71,40 @@ static void test_button_free_cancels_long_press(void) {
assert_se(!m->reboot_key_long_press_event_source);
}
/* Verify that seat_free() does not leave itself on the GC queue when draining a device re-queues it:
* device_detach() calls seat_add_to_gc_queue() once the seat loses its last master device, so a
* seat_free() that dropped itself from the queue before that drain would leave the freed seat dangling
* at the queue head for the next manager_gc() pass. */
static void test_seat_free_dequeues_on_device_detach(void) {
_cleanup_(hashmap_freep) Hashmap *seats = NULL;
_cleanup_(hashmap_freep) Hashmap *devices = NULL;
_cleanup_free_ Manager *m = NULL;
Seat *s;
Device *d;
assert_se(m = new0(Manager, 1));
assert_se(seats = hashmap_new(&string_hash_ops));
assert_se(devices = hashmap_new(&string_hash_ops));
m->seats = seats;
m->devices = devices;
assert_se(seat_new(m, "seattest", &s) >= 0);
/* A non-master device: the seat has no master, so detaching it re-queues the seat for GC. */
assert_se(d = device_new(m, "/sys/devices/platform/test-seat-device", /* master= */ false));
device_attach(d, s);
/* Mimic manager_gc() having popped the seat off the queue right before freeing it. */
assert_se(!s->in_gc_queue);
assert_se(!m->seat_gc_queue);
seat_free(s);
/* Freeing the seat drained the device, which re-queued the (now freed) seat. seat_free() must
* have removed it again, otherwise the queue head is a dangling pointer. */
assert_se(!m->seat_gc_queue);
}
int main(int argc, char **argv) {
test_setup_logging(LOG_DEBUG);
@@ -82,6 +118,7 @@ int main(int argc, char **argv) {
test_sleep_handle_action();
test_button_free_cancels_long_press();
test_seat_free_dequeues_on_device_detach();
return EXIT_SUCCESS;
}