Skip to content

Commit cfe9f76

Browse files
authored
Merge pull request #26 from LinkTed/nightly
Detect endless recursion in domain name
2 parents b85d3cd + 0a3ce5d commit cfe9f76

File tree

7 files changed

+101
-74
lines changed

7 files changed

+101
-74
lines changed

.github/workflows/ci.yml

Lines changed: 22 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -15,40 +15,29 @@ jobs:
1515
runs-on: ubuntu-latest
1616
steps:
1717
- name: Install toolchain with rustfmt
18-
uses: actions-rs/toolchain@v1
18+
uses: dtolnay/rust-toolchain@stable
1919
with:
20-
toolchain: stable
2120
components: rustfmt
22-
- uses: actions/checkout@v2
21+
- uses: actions/checkout@v4
2322
- name: Run rustfmt
2423
run: cargo fmt --all -- --check
2524

26-
audit:
27-
name: Job audit
28-
runs-on: ubuntu-latest
29-
steps:
30-
- uses: actions/checkout@v1
31-
- name: Run audit
32-
uses: actions-rs/audit-check@v1
33-
with:
34-
token: ${{ secrets.GITHUB_TOKEN }}
35-
3625
clippy:
3726
name: Job clippy
3827
needs: rustfmt
3928
runs-on: ubuntu-latest
4029
steps:
4130
- name: Install toolchain with clippy
42-
uses: actions-rs/toolchain@v1
31+
uses: dtolnay/rust-toolchain@stable
4332
with:
44-
toolchain: stable
4533
components: clippy
46-
- uses: actions/checkout@v2
34+
- uses: actions/checkout@v4
4735
- name: Run clippy
48-
uses: actions-rs/clippy-check@v1
36+
uses: giraffate/clippy-action@v1
4937
with:
50-
token: ${{ secrets.GITHUB_TOKEN }}
51-
args: -- --deny warnings -A clippy::unknown-clippy-lints
38+
reporter: 'github-pr-check'
39+
github_token: ${{ secrets.GITHUB_TOKEN }}
40+
clippy_flags: --deny warnings -A clippy::unknown-clippy-lints
5241

5342
tests:
5443
name: Job tests
@@ -60,39 +49,29 @@ jobs:
6049
runs-on: ${{ matrix.os }}
6150
steps:
6251
- name: Install toolchain ${{ matrix.rust_channel }} on ${{ matrix.os }}
63-
uses: actions-rs/toolchain@v1
52+
uses: dtolnay/rust-toolchain@master
6453
with:
6554
toolchain: ${{ matrix.rust_channel }}
66-
- uses: actions/checkout@v2
55+
- uses: actions/checkout@v4
6756
- name: Run cargo test
68-
uses: actions-rs/cargo@v1
69-
with:
70-
command: test
71-
args: --all-features
57+
run: cargo test --no-default-features --features "${{ matrix.features }}"
7258

7359
code-coverage:
7460
name: Job code coverage
7561
needs: tests
7662
runs-on: ubuntu-latest
7763
steps:
7864
- name: Intall toolchain nightly on ubuntu-latest
79-
uses: actions-rs/toolchain@v1
80-
with:
81-
toolchain: nightly
82-
override: true
83-
- uses: actions/checkout@v2
84-
- name: Run cargo test
85-
uses: actions-rs/cargo@v1
65+
uses: dtolnay/rust-toolchain@stable
8666
with:
87-
command: test
88-
args: --all-features
89-
env:
90-
CARGO_INCREMENTAL: '0'
91-
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests'
92-
RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests'
93-
- name: Run grcov
94-
uses: actions-rs/[email protected]
95-
- name: Upload coverage
96-
uses: codecov/codecov-action@v1
67+
components: llvm-tools-preview
68+
- uses: actions/checkout@v4
69+
- name: cargo install cargo-llvm-cov
70+
uses: taiki-e/install-action@cargo-llvm-cov
71+
- name: cargo llvm-cov
72+
run: cargo llvm-cov --all-features --lcov --output-path lcov.info
73+
- name: Upload to codecov.io
74+
uses: codecov/codecov-action@v4
9775
with:
98-
file: ${{ steps.coverage.outputs.report }}
76+
token: ${{ secrets.CODECOV_TOKEN }}
77+
env_vars: OS,RUST

Cargo.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "dns-message-parser"
3-
version = "0.7.0"
3+
version = "0.8.0"
44
authors = ["LinkTed <[email protected]>"]
55
edition = "2018"
66
readme = "README.md"
@@ -22,13 +22,13 @@ categories = [
2222
]
2323

2424
[dependencies]
25-
base64 = "0.13.0"
26-
bytes = "1.2.1"
27-
hex = "0.4.3"
28-
thiserror = "1.0.32"
25+
base64 = "0.22"
26+
bytes = "1"
27+
hex = "0.4"
28+
thiserror = "2"
2929

3030
[dev-dependencies]
31-
criterion = "0.3.6"
31+
criterion = "0.5"
3232

3333
[[bench]]
3434
name = "message"

src/decode/decoder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ impl<'a, 'b: 'a> Decoder<'a, 'b> {
3838
}
3939
}
4040

41-
pub(super) const fn get_main(&'a self) -> &Decoder<'a, 'b> {
41+
pub(super) const fn get_main(&self) -> &Decoder<'a, 'b> {
4242
let mut root = self;
4343
loop {
4444
match root.parent {

src/decode/domain_name.rs

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{
22
decode::Decoder, domain_name::DOMAIN_NAME_MAX_RECURSION, DecodeError, DecodeResult, DomainName,
33
};
4-
use std::str::from_utf8;
4+
use std::{collections::HashSet, str::from_utf8, usize};
55

66
const COMPRESSION_BITS: u8 = 0b1100_0000;
77
const COMPRESSION_BITS_REV: u8 = 0b0011_1111;
@@ -19,32 +19,56 @@ const fn get_offset(length_1: u8, length_2: u8) -> usize {
1919
impl<'a, 'b: 'a> Decoder<'a, 'b> {
2020
pub(super) fn domain_name(&mut self) -> DecodeResult<DomainName> {
2121
let mut domain_name = DomainName::default();
22-
self.domain_name_recursion(&mut domain_name, 0)?;
22+
23+
let mut length = self.u8()?;
24+
while length != 0 {
25+
if is_compressed(length) {
26+
let mut recursions = HashSet::new();
27+
self.domain_name_recursion(&mut domain_name, &mut recursions, length)?;
28+
return Ok(domain_name);
29+
} else {
30+
length = self.domain_name_label(&mut domain_name, length)?;
31+
}
32+
}
2333
Ok(domain_name)
2434
}
2535

36+
fn domain_name_label(&mut self, domain_name: &mut DomainName, length: u8) -> DecodeResult<u8> {
37+
let buffer = self.read(length as usize)?;
38+
let label = from_utf8(buffer.as_ref())?;
39+
let label = label.parse()?;
40+
domain_name.append_label(label)?;
41+
self.u8()
42+
}
43+
2644
fn domain_name_recursion(
2745
&mut self,
2846
domain_name: &mut DomainName,
29-
recursion: usize,
47+
recursions: &mut HashSet<usize>,
48+
mut length: u8,
3049
) -> DecodeResult<()> {
31-
if recursion > DOMAIN_NAME_MAX_RECURSION {
32-
return Err(DecodeError::MaxRecursion(recursion));
33-
}
50+
let mut buffer = self.u8()?;
51+
let mut offset = get_offset(length, buffer);
52+
let mut decoder = self.new_main_offset(offset);
53+
54+
length = decoder.u8()?;
3455

35-
let mut length = self.u8()?;
3656
while length != 0 {
3757
if is_compressed(length) {
38-
let buffer = self.u8()?;
39-
let offset = get_offset(length, buffer);
40-
let mut decoder = self.new_main_offset(offset);
41-
return decoder.domain_name_recursion(domain_name, recursion + 1);
58+
buffer = decoder.u8()?;
59+
offset = get_offset(length, buffer);
60+
if recursions.insert(offset) {
61+
let recursions_len = recursions.len();
62+
if recursions_len > DOMAIN_NAME_MAX_RECURSION {
63+
return Err(DecodeError::MaxRecursion(recursions_len));
64+
}
65+
} else {
66+
return Err(DecodeError::EndlessRecursion(offset));
67+
}
68+
decoder.offset = offset as usize;
69+
length = decoder.u8()?;
4270
} else {
43-
let buffer = self.read(length as usize)?;
44-
let label = from_utf8(buffer.as_ref())?;
45-
let label = label.parse()?;
46-
domain_name.append_label(label)?;
47-
length = self.u8()?;
71+
length = decoder.domain_name_label(domain_name, length)?;
4872
}
4973
}
5074

src/decode/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ pub enum DecodeError {
8989
DNSKEYProtocol(u8),
9090
#[error("Could not decode the domain name, the because maximum recursion is reached: {0}")]
9191
MaxRecursion(usize),
92+
#[error("Could not decode the domain name, because an endless recursion was detected: {0}")]
93+
EndlessRecursion(usize),
9294
#[error("The are remaining bytes, which was not parsed")]
9395
RemainingBytes(usize, Dns),
9496
#[error("Padding is not zero: {0}")]

src/rr/draft_ietf_dnsop_svcb_https.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
use std::cmp::Ordering;
2-
use std::fmt::{Display, Formatter, Result as FmtResult};
3-
use std::hash::{Hash, Hasher};
4-
use std::net::{Ipv4Addr, Ipv6Addr};
5-
61
use crate::rr::draft_ietf_dnsop_svcb_https::ServiceBindingMode::{Alias, Service};
72
use crate::rr::{ToType, Type};
83
use crate::DomainName;
4+
use base64::{engine::general_purpose::STANDARD as Base64Standard, Engine};
5+
use std::cmp::Ordering;
96
use std::collections::BTreeSet;
7+
use std::fmt::{Display, Formatter, Result as FmtResult};
8+
use std::hash::{Hash, Hasher};
9+
use std::net::{Ipv4Addr, Ipv6Addr};
1010

1111
/// A Service Binding record for locating alternative endpoints for a service.
1212
///
@@ -227,7 +227,7 @@ impl Display for ServiceParameter {
227227
)
228228
}
229229
ServiceParameter::ECH { config_list } => {
230-
write!(f, "ech={}", base64::encode(config_list))
230+
write!(f, "ech={}", Base64Standard.encode(config_list))
231231
}
232232
ServiceParameter::IPV6_HINT { hints } => {
233233
write!(
@@ -261,7 +261,7 @@ impl Display for ServiceParameter {
261261
if let Ok(value) = String::from_utf8(escaped) {
262262
write!(f, "{}=\"{}\"", key, value)
263263
} else {
264-
write!(f, "{}=\"{}\"", key, base64::encode(wire_data))
264+
write!(f, "{}=\"{}\"", key, Base64Standard.encode(wire_data))
265265
}
266266
}
267267
}

tests/decode_error.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use bytes::Bytes;
22
use dns_message_parser::{
33
question::{QClass, QType, Question},
44
rr::{AddressError, Class, TagError},
5-
Opcode, RCode, MAXIMUM_DNS_PACKET_SIZE, {DecodeError, Dns, Flags},
5+
DecodeError, Dns, DomainName, Flags, Opcode, RCode, MAXIMUM_DNS_PACKET_SIZE,
66
};
77

88
fn decode_msg_error(msg: &[u8], e: DecodeError) {
@@ -23,6 +23,15 @@ fn decode_flags_error(msg: &[u8], e: DecodeError) {
2323
assert_eq!(flags, Err(e))
2424
}
2525

26+
fn decode_domain_name_error(msg: &[u8], e: DecodeError) {
27+
// Decode BytesMut to message
28+
let bytes = Bytes::copy_from_slice(msg);
29+
// Decode the domain name
30+
let domain_name = DomainName::decode(bytes);
31+
// Check the result
32+
assert_eq!(domain_name, Err(e))
33+
}
34+
2635
#[test]
2736
fn flags_1() {
2837
let msg = b"";
@@ -53,6 +62,19 @@ fn flags_5() {
5362
decode_flags_error(msg, DecodeError::RCode(15));
5463
}
5564

65+
#[test]
66+
fn domain_name_1() {
67+
let msg = b"\xc0\x02\xc0\x00";
68+
decode_domain_name_error(msg, DecodeError::EndlessRecursion(0));
69+
}
70+
71+
#[test]
72+
fn domain_name_2() {
73+
let msg =
74+
b"\xc0\x02\xc0\x04\xc0\x06\xc0\x08\xc0\x0a\xc0\x0c\xc0\x0e\xc0\x10\xc0\x12\xc0\x14\xc0\x16\xc0\x18\xc0\x1a\xc0\x1c\xc0\x1e\xc0\x20\xc0\x22\xc0\x24";
75+
decode_domain_name_error(msg, DecodeError::MaxRecursion(17));
76+
}
77+
5678
#[test]
5779
fn dns_not_enough_bytes() {
5880
let msg = b"";

0 commit comments

Comments
 (0)