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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
//! Protected writes for user-owned integration configuration, not managed assets.
use std::fs::{self, OpenOptions};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(test)]
mod tests;
static NEXT_TEMP: AtomicU64 = AtomicU64::new(0);
/// Check before changing assets as well as immediately before replacing a config.
/// This is deliberately not config parsing or a transaction across multiple files.
pub(super) fn check_config_targets(dir: &Path, names: &[&str]) -> io::Result<()> {
for name in names {
check_config_target(&dir.join(name))?;
}
Ok(())
}
pub(super) fn check_config_target(path: &Path) -> io::Result<()> {
reject_hard_links(path)?;
crate::platform::check_config_write_target(&resolve_target(path)?)
}
fn reject_hard_links(path: &Path) -> io::Result<()> {
let metadata = match fs::metadata(path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(error),
};
if metadata.is_file() && crate::platform::config_file_link_count(path)? > 1 {
return Err(io::Error::other(format!(
"cannot update {}: config has multiple hard links; use a separate file or a symlink before retrying",
path.display()
)));
}
Ok(())
}
// Unlike canonicalize, this also follows dangling symlinks on a first install.
fn resolve_target(path: &Path) -> io::Result<PathBuf> {
let mut current = path.to_path_buf();
for _ in 0..40 {
match fs::symlink_metadata(¤t) {
Ok(metadata) if metadata.file_type().is_symlink() => {
let link = fs::read_link(¤t)?;
current = if link.is_absolute() {
link
} else {
current.parent().unwrap_or(Path::new(".")).join(link)
};
}
Ok(metadata) if !metadata.is_file() => {
return Err(io::Error::other(format!(
"cannot update {}: config is not a regular file",
path.display()
)));
}
Ok(_) => return Ok(current),
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(current),
Err(error) => return Err(error),
}
}
Err(io::Error::other(format!(
"cannot update {}: too many symbolic links",
path.display()
)))
}
pub(super) fn write_config(path: &Path, contents: impl AsRef<[u8]>) -> io::Result<()> {
check_config_target(path)?;
let target = resolve_target(path)?;
if crate::platform::write_existing_config(&target, contents.as_ref())? {
return Ok(());
}
let replacement = Replacement::prepare(&target, contents.as_ref())?;
replacement.commit()
}
struct Replacement {
target: PathBuf,
temporary: PathBuf,
}
impl Replacement {
fn prepare(path: &Path, contents: &[u8]) -> io::Result<Self> {
reject_hard_links(path)?;
let target = resolve_target(path)?;
let existing = match fs::metadata(&target) {
Ok(_) => {
// A writable parent must not let rename bypass file write permissions.
OpenOptions::new().read(true).write(true).open(&target)?;
Some(target.as_path())
}
Err(error) if error.kind() == io::ErrorKind::NotFound => None,
Err(error) => return Err(error),
};
let parent = target
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or(Path::new("."));
for _ in 0..128 {
let sequence = NEXT_TEMP.fetch_add(1, Ordering::Relaxed);
let temporary = parent.join(format!(
".herdr-config-{}-{sequence}.tmp",
std::process::id()
));
// Existing configs can contain secrets. Start their staging file private;
// the platform writer preserves the original permissions before publication.
// New configs retain ordinary create/umask/inherited-ACL defaults.
let created = crate::platform::create_config_temporary(&temporary, existing.is_some());
match created {
Ok(file) => drop(file),
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
Err(error) => return Err(error),
}
let replacement = Self {
target: target.clone(),
temporary,
};
crate::platform::write_config_temporary(existing, &replacement.temporary, contents)?;
return Ok(replacement);
}
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"could not allocate a unique config temporary file",
))
}
fn commit(self) -> io::Result<()> {
reject_hard_links(&self.target)?;
crate::platform::replace_file(&self.temporary, &self.target)
}
}
impl Drop for Replacement {
fn drop(&mut self) {
// After publication the temporary name is absent. Never remove the target.
if let Err(error) = fs::remove_file(&self.temporary) {
if error.kind() != io::ErrorKind::NotFound {
tracing::warn!(path = %self.temporary.display(), %error, "failed to remove integration config temporary file");
}
}
}
}
|