Home | History | Annotate | Line # | Download | only in ssutoctou
      1 # Copyright (C) Internet Systems Consortium, Inc. ("ISC")
      2 #
      3 # SPDX-License-Identifier: MPL-2.0
      4 #
      5 # This Source Code Form is subject to the terms of the Mozilla Public
      6 # License, v. 2.0.  If a copy of the MPL was not distributed with this
      7 # file, you can obtain one at https://mozilla.org/MPL/2.0/.
      8 #
      9 # See the COPYRIGHT file distributed with this work for additional
     10 # information regarding copyright ownership.
     11 
     12 """
     13 Regression test for GL#5006: TOCTOU race in DNS UPDATE SSU table handling.
     14 
     15 send_update() and update_action() used to independently read the zone's
     16 SSU table.  If rndc reconfig changed the zone's update policy between
     17 these two reads, the values could diverge, causing an assertion failure.
     18 
     19 This test races rndc reconfig (toggling between allow-update and
     20 update-policy) against a stream of DNS UPDATEs to verify that named
     21 survives without crashing.
     22 """
     23 
     24 import threading
     25 import time
     26 
     27 import dns.query
     28 import dns.rdatatype
     29 import dns.update
     30 import pytest
     31 
     32 import isctest
     33 
     34 pytestmark = pytest.mark.extra_artifacts(
     35     [
     36         "*/*.db",
     37         "*/*.jnl",
     38     ]
     39 )
     40 
     41 
     42 def send_updates(ip, port, stop_event):
     43     """Send DNS UPDATEs in a tight loop until stopped."""
     44     n = 0
     45     while not stop_event.is_set():
     46         n += 1
     47         try:
     48             up = dns.update.UpdateMessage("example.")
     49             up.add(
     50                 f"test{n}.example.",
     51                 300,
     52                 dns.rdatatype.A,
     53                 f"10.0.0.{n % 256}",
     54             )
     55             dns.query.tcp(up, ip, port=port, timeout=2)
     56         except Exception:  # pylint: disable=broad-exception-caught
     57             pass
     58 
     59 
     60 def toggle_config(ns1, templates, stop_event):
     61     """Toggle zone config between allow-update and update-policy."""
     62     use_ssu = False
     63     while not stop_event.is_set():
     64         use_ssu = not use_ssu
     65         try:
     66             templates.render("ns1/named.conf", {"use_ssu": use_ssu})
     67             ns1.rndc("reconfig")
     68         except Exception:  # pylint: disable=broad-exception-caught
     69             pass
     70         time.sleep(0.01)
     71 
     72 
     73 def test_ssu_toctou_race(ns1, templates):
     74     """Race rndc reconfig against DNS UPDATEs -- named must not crash."""
     75     port = int(isctest.vars.ALL["PORT"])
     76     stop = threading.Event()
     77 
     78     update_thread = threading.Thread(
     79         target=send_updates,
     80         args=("10.53.0.1", port, stop),
     81     )
     82     reconfig_thread = threading.Thread(
     83         target=toggle_config,
     84         args=(ns1, templates, stop),
     85     )
     86 
     87     update_thread.start()
     88     reconfig_thread.start()
     89 
     90     # Let them race for a few seconds
     91     time.sleep(5)
     92 
     93     stop.set()
     94     update_thread.join(timeout=10)
     95     reconfig_thread.join(timeout=10)
     96 
     97     # Restore original config
     98     templates.render("ns1/named.conf", {"use_ssu": False})
     99     ns1.rndc("reconfig")
    100 
    101     # Verify named is still alive
    102     msg = isctest.query.create("ns.example.", "A")
    103     res = isctest.query.udp(msg, "10.53.0.1")
    104     isctest.check.noerror(res)
    105