From 556a19cf0727712830b7a0c16b2f22aa87c2ce6f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20H=C3=A1k?= <jan.hak@nic.cz>
Date: Fri, 8 Nov 2024 14:06:26 +0100
Subject: [PATCH] knotd: expiration aborts transaction in progress

---
 src/knot/events/handlers/expire.c             |  7 +-
 .../tests/ctl/expire_abort/data/expire.zone   |  5 ++
 tests-extra/tests/ctl/expire_abort/test.py    | 64 +++++++++++++++++++
 3 files changed, 75 insertions(+), 1 deletion(-)
 create mode 100644 tests-extra/tests/ctl/expire_abort/data/expire.zone
 create mode 100644 tests-extra/tests/ctl/expire_abort/test.py

diff --git a/src/knot/events/handlers/expire.c b/src/knot/events/handlers/expire.c
index 5124e4acb7..3e8b6f68b3 100644
--- a/src/knot/events/handlers/expire.c
+++ b/src/knot/events/handlers/expire.c
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -32,6 +32,11 @@ int event_expire(conf_t *conf, zone_t *zone)
 	log_zone_info(zone->name, "zone expired");
 
 	synchronize_rcu();
+
+	pthread_mutex_lock(&zone->cu_lock);
+	zone_control_clear(zone);
+	pthread_mutex_unlock(&zone->cu_lock);
+
 	knot_sem_wait(&zone->cow_lock);
 	zone_contents_deep_free(expired);
 	knot_sem_post(&zone->cow_lock);
diff --git a/tests-extra/tests/ctl/expire_abort/data/expire.zone b/tests-extra/tests/ctl/expire_abort/data/expire.zone
new file mode 100644
index 0000000000..0b21bd0c8f
--- /dev/null
+++ b/tests-extra/tests/ctl/expire_abort/data/expire.zone
@@ -0,0 +1,5 @@
+$ORIGIN expire.
+
+@	SOA	dns1 hostmaster 2010111201 2 1 4 7200
+	NS	dns1
+dns1	A	192.0.2.1
diff --git a/tests-extra/tests/ctl/expire_abort/test.py b/tests-extra/tests/ctl/expire_abort/test.py
new file mode 100644
index 0000000000..c794313363
--- /dev/null
+++ b/tests-extra/tests/ctl/expire_abort/test.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python3
+
+'''Test for automatic zone transaction abort when zone expires'''
+
+from dnstest.libknot import libknot
+from dnstest.test import Test
+from dnstest.utils import *
+
+def check_txn(server, zone, is_open):
+    ctl = libknot.control.KnotCtl()
+    ctl.connect(os.path.join(slave.dir, "knot.sock"))
+
+    ctl.send_block(cmd="zone-status", zone=zone, flags="B", filters="t")
+    resp = ctl.receive_block()
+    if is_open:
+        isset(resp[zone]["transaction"] == "open", "open transaction")
+    else:
+        isset(resp[zone]["transaction"] == "-", "no transaction")
+
+    ctl.send(libknot.control.KnotCtlType.END)
+    ctl.close()
+
+def test_expire(master, slave, zone, manual_expiration):
+    slave.ctl("zone-refresh")
+    slave.zone_wait(zone)
+    master.stop()
+
+    slave.ctl("zone-begin expire")
+    check_txn(slave, zone[0].name, True)
+    slave.ctl("zone-set expire test TXT test")
+
+    if manual_expiration:
+        slave.ctl("-f zone-purge +expire expire")
+    else:
+        t.sleep(5)
+
+    try:
+        slave.ctl("zone-commit expire")
+        set_err("control txn not aborted")
+    except Exception:
+        pass
+
+    check_txn(slave, zone[0].name, False)
+    resp = slave.dig("test.expire", "TXT")
+    resp.check(rcode="SERVFAIL")
+
+    master.start()
+
+t = Test()
+
+master = t.server("knot")
+slave = t.server("knot")
+zone = t.zone("expire.", storage=".")
+t.link(zone, master, slave)
+
+t.start()
+
+# Expire manually
+test_expire(master, slave, zone, True)
+
+# Expire by the SOA timer
+test_expire(master, slave, zone, False)
+
+t.end()
-- 
GitLab