1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// Copyright 2024 Jeff Kim <hiking90@gmail.com>
// SPDX-License-Identifier: Apache-2.0
//! Shared on-disk metadata validation for the mmap'd property files
//! (`property_info`, per-context areas, `properties_serial`).
use crate::errors::{Error, Result};
/// Validates file metadata for system property files.
///
/// In test and debug modes, only checks file permissions and size.
/// In production mode, also enforces that the file is owned by root (uid=0, gid=0).
pub(crate) fn validate_file_metadata(
metadata: &std::fs::Metadata,
path: &std::path::Path,
min_size: u64,
) -> Result<()> {
// The portable-across-unix trait (uniform `u32`/`u64` accessors), not
// the per-OS `st_*` variants — those needed three cfg'd imports plus a
// macOS-only integer cast, and broke the build on every other unix.
use std::os::unix::fs::MetadataExt;
// Only regular files are acceptable mmap targets. The metadata comes
// from an already-open fd (fstat), so this check is race-free; without
// it a directory fails later at mmap with a confusing ENODEV, and a
// device file could be mapped successfully despite not being a
// trustworthy property file.
if !metadata.is_file() {
let error_msg = format!("Not a regular file: {path:?}");
log::error!("{error_msg}");
return Err(Error::FileValidation(error_msg));
}
// Check file size first (applies to all modes)
if metadata.size() < min_size {
let error_msg = format!(
"File too small: size={}, min_size={} for {:?}",
metadata.size(),
min_size,
path
);
log::error!("{error_msg}");
return Err(Error::FileSize(error_msg));
}
// Check write permissions (applies to all modes). 0o022 = the
// group-write | other-write bits (S_IWGRP | S_IWOTH).
let writable_by_others = metadata.mode() & 0o022;
if writable_by_others != 0 {
let error_msg = format!(
"File has group or other write permissions: mode={:#o} for {:?}",
metadata.mode(),
path
);
log::error!("{error_msg}");
return Err(Error::PermissionDenied(error_msg));
}
// In production (release) builds, also check ownership.
//
// `cfg!(test)` is intentionally NOT part of this condition: it is only
// true while compiling this crate's own `--test` harness, so it never
// covers integration tests, doctests, or downstream crates — relying
// on it made `cargo test` and `cargo test --release` behave
// differently for the same fixture files. `debug_assertions` alone
// draws the line uniformly at dev-vs-release.
//
// `debug-assertions` is not a security switch, though: a release
// profile with `debug-assertions = true` (commonly enabled for
// overflow checks — this workspace's bench profile does exactly that)
// would silently disable the check as a side effect. Deployments in
// that situation opt back in explicitly with the
// `strict-file-validation` feature, which enforces ownership
// regardless of the profile. The remaining skip is logged so it is
// observable either way.
let skip_ownership_check = cfg!(debug_assertions) && !cfg!(feature = "strict-file-validation");
if skip_ownership_check {
// AtomicBool, not `Once`: a logger backed by property reads would
// re-enter this function from inside its own log call, and
// re-entering an in-progress `call_once` on the same thread is a
// deadlock by contract. A lost race here just means one extra warn.
static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
log::warn!(
"root-ownership check on property files is disabled \
(debug-assertions build)"
);
}
}
if !skip_ownership_check && (metadata.uid() != 0 || metadata.gid() != 0) {
let error_msg = format!(
"File not owned by root: uid={}, gid={} for {:?}",
metadata.uid(),
metadata.gid(),
path
);
log::error!("{error_msg}");
return Err(Error::FileOwnership(error_msg));
}
Ok(())
}