Skip to content

[dhcp4relay] Security and reliability fixes - #119

Open
ashutosh-agrawal wants to merge 6 commits into
sonic-net:masterfrom
ashutosh-agrawal:upstream/dhcp4relay-security-fixes-clean
Open

[dhcp4relay] Security and reliability fixes#119
ashutosh-agrawal wants to merge 6 commits into
sonic-net:masterfrom
ashutosh-agrawal:upstream/dhcp4relay-security-fixes-clean

Conversation

@ashutosh-agrawal

@ashutosh-agrawal ashutosh-agrawal commented Jul 18, 2026

Copy link
Copy Markdown
Member

Why I did it

The DHCPv4 relay has several reliability and memory-safety issues in configuration processing and packet handling:

  • SubscriberStateTable instances for both relay and DHCP-server configuration are always registered with swss::Select, but events for the inactive mode were not popped. Their cached keyspace events therefore remained pending, causing select() to return immediately and the configuration thread to consume a CPU core.
  • Option 82 is constructed in a fixed-size buffer. Long circuit IDs, remote IDs, or VRF-derived sub-options could exceed the remaining buffer capacity or encode a wrapped one-byte length, producing malformed relay metadata or writing past the buffer.
  • A BOOTPREPLY arriving on an interface that is also configured as a client-facing VLAN member was accepted as a server reply. This violates the intended server/client ingress separation and can forward a reply received from the wrong direction.
  • The configuration listener updates metadata, DHCP-server mode, and server address state while the packet path reads the same values. Concurrent access to std::string-backed metadata is undefined behavior and can result in corrupted Option 82 data or a daemon crash.
  • DhcpLayer headers were accessed before validating that a received frame contains a complete DHCP header. In addition, BOOTP minimum-length padding could extend the caller-owned packet buffer instead of a bounded local buffer. Truncated or short frames could therefore trigger out-of-bounds reads or writes.

How I did it

  • Drain every selected CONFIG_DB subscriber before deciding whether to process its event for the active relay mode.
  • Add bounds checks to Option 82 TLV encoding, including circuit ID, remote ID, and VRF sub-options.
  • Reject BOOTPREPLY packets received on interfaces configured as client-facing VLAN members.
  • Synchronize shared metadata and DHCP server state between the configuration and packet-processing threads using a mutex and atomic feature flag.
  • Validate DHCP layer lengths before accessing headers, clear stale receive-buffer bytes, and pad BOOTP packets using a local fixed-size buffer.
  • Add unit coverage for oversized Option 82 fields, client-facing reply ingress, short DHCP frames, and BOOTP padding.

How to verify it

Verify that:

  • DHCP relay processes CONFIG_DB updates without sustained CPU spin.
  • Oversized Option 82 values are rejected safely without corrupting packet data.
  • Replies arriving on client-facing interfaces are dropped.
  • Short DHCP frames are dropped safely.
  • Existing DHCP relay functional and stress tests continue to pass.

…id config-thread busy-loop

handle_swss_notification() registers the DHCP_SERVER_IPV4 and
DHCP_SERVER_IPV4_SERVER_IP subscriber tables with swss::Select regardless of
feature_dhcp_server_enabled, but only pops() them inside the feature-enabled
branch. When the feature is disabled (the default), two or more writes to
those tables leave their keyspace-event buffer undrained; SubscriberStateTable::
hasCachedData() then stays true, so swss::Select keeps re-queueing the
selectable and select() never blocks, pinning one CPU core. The mirror gap
exists for the relaymgr/interface tables when the feature is enabled.

Flatten the dispatch into a single if/else-if chain that always pops() the
selectable that fired and gates only the processing on
feature_dhcp_server_enabled, so every buffer drains in both modes. Events for
the inactive mode are discarded; the subscriber is recreated (and its snapshot
re-read) whenever process_feature_notification toggles the feature, so no
state is lost.

(cherry picked from commit 51ce4a8c1b5ef3ecc373917aa9f4f403cf479b8e)
Signed-off-by: Ashutosh Agrawal <ashu@cisco.com>
…tion (sonic-net#54)

* MIGSOFTWAR-45218: dhcp4relay: fix buffer overflows in encode_relay_option

Three bounds-checking fixes in encode_relay_option():

1. circuit-id: drop option 82 entirely if circuit-id exceeds 253 bytes
   to prevent writing past buf[256].

2. Remote-ID: use std::min(MAC_ADDR_STR_LEN, mac.length()) so a short
   MAC string is not read past its end.

3. VSS sub-option: skip the sub-option and log a warning if the VRF
   name exceeds vss_buf[32]-1 bytes instead of overflowing the buffer.

Add unit tests for all three cases.

* MIGSOFTWAR-45218: fix uint8_t wrap and test gaps flagged in review

- Guard circuit_id.length() > UINT8_MAX before the uint8_t cast to
  prevent a silent wrap (e.g. 256 bytes → length field 0) that would
  produce a malformed zero-length circuit-id TLV
- Add remaining arg to EncodeAndDecode unit test (compile fix)
- Assert has_remote_id after loop in encode_relay_option_short_mac so
  the test fails if remote-id is absent rather than silently passing

* MIGSOFTWAR-45218: [dhcp4relay] drop option 82 when required remote-id does not fit

* MIGSOFTWAR-45218: [dhcp4relay] drop option 82 when configured sub-options do not fit

* MIGSOFTWAR-45218: [dhcp4relay] fix off-by-one bound in decode_tlv

* MIGSOFTWAR-45218: fix narrowing uint8_t → size_t for encode_tlv return in test

(cherry picked from commit da704dbe49de7721edb2aa9e52c0ce5feda9bf65)
Signed-off-by: Ashutosh Agrawal <ashu@cisco.com>
…interfaces (sonic-net#58)

* MIGSOFTWAR-45222b: dhcp4relay: reject server replies on client-facing interfaces

Pass ingress_intf to to_client() and drop any BOOTPREPLY arriving on a
physical interface registered in vlan_map as a client-facing VLAN member.
This reinstates the structural server/client interface separation that
ISC dhcrelay enforced via -id/-iu flags.

Tests: to_client declaration and call updated; to_client_ingress_on_client_vlan added.

* MIGSOFTWAR-45222: [dhcp4relay] fix stray brace in to_client test

* MIGSOFTWAR-45222: [dhcp4relay] make ingress rejection test regression-proof

(cherry picked from commit 90be4074ed5eb23495a1341ea4835f43e99f0b7c)
Signed-off-by: Ashutosh Agrawal <ashu@cisco.com>
…n threads (sonic-net#63)

* MIGSOFTWAR-45221: [dhcp4relay] synchronize shared config state between threads

The config-listener thread writes m_config (hostname, host MAC, midplane
bridge), feature_dhcp_server_enabled and global_dhcp_server_ip, while the
libevent packet thread reads them on the per-packet path with no lock.
Concurrent std::string assignment and read is undefined behavior and can
corrupt the option-82 encoding or abort the daemon.

Guard the shared state with a single m_config_mutex: writers build a local
copy and swap it in under the lock (keeping the lock off the midplane DB
read), and packet-path readers take a snapshot under the lock. Make
feature_dhcp_server_enabled a std::atomic<bool>, and update
global_dhcp_server_ip under the lock before signaling the main thread so it
never observes a stale value.

* MIGSOFTWAR-45221: [dhcp4relay] commit m_config before DualToR event; align mock atomic decl

* MIGSOFTWAR-45221: [dhcp4relay] guard all global_dhcp_server_ip access; rollback on pipe failure

* MIGSOFTWAR-45221: [dhcp4relay] update subtype flags only when subtype field changes

* MIGSOFTWAR-45221: clear DualToR/SmartSwitch flags when subtype field is deleted

SubscriberStateTable delivers all current hash fields on every notification,
so subtype_found=false in a SET means the field was deleted, not just absent
from a partial update.  Add an else branch that clears is_dualTor and
is_SmartSwitch when subtype is not present, and fires the DualToR pipe event
so the main thread stops using Loopback0/link-selection behavior.

---------

(cherry picked from commit cc4e1da914123dab3a68c63a0c33bbc9eda4c45a)
Signed-off-by: Ashutosh Agrawal <ashu@cisco.com>
…short frames (sonic-net#62)

* MIGSOFTWAR-45223: dhcp4relay: fix heap overflow in BOOTP padding for short frames

1. send_udp(): copy caller's buffer into a local 300-byte stack array before
   padding to BOOTP_MIN_LEN instead of writing past the caller's allocation.

2. from_client()/to_client(): reject any DhcpLayer whose getHeaderLen() is
   smaller than sizeof(pcpp::dhcp_header) before dereferencing getDhcpHeader()
   fields or calling addOption/removeOption.

3. pkt_in_callback(): clear client_recv_buffer bytes past buffer_sz after
   each recvmsg to prevent header bytes of a short frame from being primed
   by a preceding frame in the same batch.

* MIGSOFTWAR-45223: [dhcp4relay] reject short DHCP layer in pkt_in_callback before header access

* MIGSOFTWAR-45223: [dhcp4relay] strengthen short-packet unit tests against false positives

* MIGSOFTWAR-45223: [dhcp4relay] fix short-packet test mocks and giaddr writes

* MIGSOFTWAR-45223: fix ASAN invalid-free in short-header tests; add bootp_pad unit tests

pcpp::Layer v24.09 always calls delete[] m_Data when !isAllocatedToPacket(),
so constructing DhcpLayer over a stack array causes an ASAN invalid-free.
Switch both short-header tests to heap-allocated buffers (new uint8_t[50]()).

Extract the BOOTP minimum-length padding into bootp_pad(), compiled outside
real send_udp().  Add three unit tests covering the pad/no-pad/disabled paths.
Update send_udp() to delegate to bootp_pad().

* MIGSOFTWAR-45223: fix dhcp4_sender.h include path in unit tests

The test Makefile does not add src/ to the include path, so mock_relay.cpp
must use the same ../src/ relative include style as mock_relay.h.

---------

(cherry picked from commit b7cfa81be16902dbcd0f90f7fdc407a187c7a3c9)
Signed-off-by: Ashutosh Agrawal <ashu@cisco.com>
@mssonicbld

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

…-security-fixes-clean

Resolve link-selection conflict with sonic-net#121: adopt the ISC-aligned unmasked
VLAN address (RFC 3527) while keeping the bounds-checked encode_tlv and
thread-safe is_dualTor snapshot from this branch.

Signed-off-by: Ashutosh Agrawal <ashu@cisco.com>
@mssonicbld

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@ashutosh-agrawal

Copy link
Copy Markdown
Member Author

@Xichen96 @StormLiangMS This PR consists of 5 separate security and reliability enhancements. Please let me know if you would prefer these to be split into individual PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants