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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
//! Deterministic policies for memory-management advice.
use reverie::Error;
use reverie::Guest;
use reverie::syscalls;
use reverie::syscalls::AddrMut;
use reverie::syscalls::Errno;
use reverie::syscalls::MemoryAccess;
use crate::Detcore;
use crate::RecordOrReplay;
const PAGE_SIZE: usize = 4096;
// Added in Linux 6.13 and not yet exposed by the pinned libc crate.
const MADV_GUARD_INSTALL: i32 = 102;
const MADV_GUARD_REMOVE: i32 = 103;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum MadviseAction {
ForwardHint,
ForwardSemantic,
Ignore,
Reject(Errno),
Unknown,
}
const fn madvise_action(advice: i32) -> MadviseAction {
match advice {
// Pure access-pattern and prefetch hints have no required memory-content
// side effect. Backends without native madvise support accept them as no-ops.
libc::MADV_NORMAL | libc::MADV_RANDOM | libc::MADV_SEQUENTIAL | libc::MADV_WILLNEED => {
MadviseAction::ForwardHint
}
// Operations with guest-visible memory, fork, backing-store, dump, or guard
// semantics must reach a backend that implements native madvise behavior.
libc::MADV_DONTNEED
| libc::MADV_DONTFORK
| libc::MADV_DOFORK
| libc::MADV_DONTDUMP
| libc::MADV_DODUMP
| libc::MADV_WIPEONFORK
| libc::MADV_KEEPONFORK
| libc::MADV_DONTNEED_LOCKED
| MADV_GUARD_INSTALL
| MADV_GUARD_REMOVE => MadviseAction::ForwardSemantic,
// These are optional reclaim or asynchronous VM-policy controls. Their host
// effects depend on memory pressure, KSM, and THP activity. Hermit accepts
// them as fixed no-ops after deterministic argument validation; it deliberately
// does not reproduce each advice's host- and mapping-specific EINVAL cases.
libc::MADV_FREE
| libc::MADV_MERGEABLE
| libc::MADV_UNMERGEABLE
| libc::MADV_HUGEPAGE
| libc::MADV_NOHUGEPAGE
| libc::MADV_COLD
| libc::MADV_PAGEOUT => MadviseAction::Ignore,
// Hole punching mutates backing storage and every mapping alias. Refuse it
// until Detcore can update file resources and replay all affected aliases.
libc::MADV_REMOVE => MadviseAction::Reject(Errno::EINVAL),
// Successful population and collapse promise synchronous, resource-
// dependent work. Report the same deterministic error Linux uses when
// an advice value is unsupported so callers can take their fallback.
libc::MADV_POPULATE_READ | libc::MADV_POPULATE_WRITE | libc::MADV_COLLAPSE => {
MadviseAction::Reject(Errno::EINVAL)
}
// Never let a guest inject host memory failures, even if the container
// unexpectedly has enough privilege to make these operations succeed.
libc::MADV_HWPOISON | libc::MADV_SOFT_OFFLINE => MadviseAction::Reject(Errno::EPERM),
_ => MadviseAction::Unknown,
}
}
fn validate_common_args(call: syscalls::Madvise) -> Result<(), Error> {
let start = call.addr().map(AddrMut::as_raw).unwrap_or(0);
if !start.is_multiple_of(PAGE_SIZE) {
return Err(Errno::EINVAL.into());
}
if call.len() == 0 {
return Ok(());
}
let end = start.checked_add(call.len()).ok_or(Errno::EINVAL)?;
end.checked_add(PAGE_SIZE - 1).ok_or(Errno::EINVAL)?;
Ok(())
}
impl<T: RecordOrReplay> Detcore<T> {
/// Apply a deterministic policy to madvise(2).
///
/// Ptrace/DBT forward hints and supported advice with guest-visible semantics.
/// Record/replay accepts pure hints as no-ops and passes guest-semantic advice
/// to the recorder and replayer, which record and restore the effects that
/// differ because replay replaces file mappings with anonymous mappings.
/// Reclaim and asynchronous VM-policy advice receives fixed success without exposing host memory pressure. Resource-
/// dependent, backing-store, and hardware-failure operations receive fixed errors.
/// KVM accepts pure hints as no-ops and reports ENOSYS for guest-visible semantics
/// its executor cannot provide.
// AUTONOMOUS-BOT-IMPLEMENTED
// TODO-HUMAN-REVIEW(#548): Recheck advice policy and record/replay boundaries.
pub async fn handle_madvise<G: Guest<Self>>(
&self,
guest: &mut G,
call: syscalls::Madvise,
) -> Result<i64, Error> {
let advice = call.advice();
let action = madvise_action(advice);
validate_common_args(call)?;
if call.len() == 0 {
return match action {
MadviseAction::Unknown => Err(Errno::EINVAL.into()),
_ => Ok(0),
};
}
if self.cfg.recordreplay_modes && action == MadviseAction::ForwardHint {
crate::detlog!(
"[dtid {}] madvise hint {} accepted as record/replay no-op",
guest.thread_state().dettid,
advice,
);
return Ok(0);
}
match action {
MadviseAction::ForwardHint if self.cfg.backend_supports_madvise => {
Ok(self.record_or_replay(guest, call).await?)
}
MadviseAction::ForwardHint => {
crate::detlog!(
"[dtid {}] madvise hint {} accepted as backend no-op",
guest.thread_state().dettid,
advice,
);
Ok(0)
}
MadviseAction::ForwardSemantic if self.cfg.backend_supports_madvise => {
Ok(self.record_or_replay(guest, call).await?)
}
MadviseAction::ForwardSemantic => {
crate::detlog!(
"[dtid {}] madvise advice {} is unsupported by this backend",
guest.thread_state().dettid,
advice,
);
Err(Errno::ENOSYS.into())
}
MadviseAction::Ignore => {
crate::detlog!(
"[dtid {}] madvise advice {} accepted as deterministic no-op",
guest.thread_state().dettid,
advice,
);
Ok(0)
}
MadviseAction::Reject(errno) => {
crate::detlog!(
"[dtid {}] madvise advice {} rejected with {}",
guest.thread_state().dettid,
advice,
errno,
);
Err(errno.into())
}
MadviseAction::Unknown => {
crate::detlog!(
"[dtid {}] unknown madvise advice {} rejected with EINVAL",
guest.thread_state().dettid,
advice,
);
Err(Errno::EINVAL.into())
}
}
}
/// Deterministic `mincore(2)`.
///
/// Real page residency reflects host memory pressure and is therefore
/// nondeterministic, which is why mincore was previously classified as an
/// unsupported syscall. GNU grep (and other glibc consumers) invoke mincore
/// under the KVM backend on a code path the ptrace backend does not take, so
/// leaving it unsupported aborts the guest under `--strict`.
///
/// Inject the call first so the backend preserves Linux pointer and mapping
/// validation, then report every mapped page as resident. The residency
/// vector is only an advisory hint, so replacing those nondeterministic bits
/// with a constant answer remains bitwise-identical across runs.
// AUTONOMOUS-BOT-IMPLEMENTED
// TODO-HUMAN-REVIEW(#775): Review deterministic mincore residency emulation.
pub async fn handle_mincore<G: Guest<Self>>(
&self,
guest: &mut G,
call: syscalls::Mincore,
) -> Result<i64, Error> {
// Linux rejects a start address that is not page-aligned with EINVAL.
let start = call.addr().map(AddrMut::as_raw).unwrap_or(0);
if !start.is_multiple_of(PAGE_SIZE) {
return Err(Errno::EINVAL.into());
}
let len = call.len();
if len == 0 {
return Ok(0);
}
// Match madvise's overflow guard on the requested range.
start.checked_add(len).ok_or(Errno::EINVAL)?;
let page_count = len.div_ceil(PAGE_SIZE);
let vec = call.vec().ok_or(Errno::EFAULT)?;
guest.inject(syscalls::Syscall::from(call)).await?;
let residency = vec![1u8; page_count];
guest.memory().write_exact(vec, &residency)?;
Ok(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn madvise_known_linux_advice_has_an_explicit_policy() {
for advice in [
libc::MADV_NORMAL,
libc::MADV_RANDOM,
libc::MADV_SEQUENTIAL,
libc::MADV_WILLNEED,
] {
assert_eq!(madvise_action(advice), MadviseAction::ForwardHint);
}
for advice in [
libc::MADV_DONTNEED,
libc::MADV_DONTFORK,
libc::MADV_DOFORK,
libc::MADV_DONTDUMP,
libc::MADV_DODUMP,
libc::MADV_WIPEONFORK,
libc::MADV_KEEPONFORK,
libc::MADV_DONTNEED_LOCKED,
MADV_GUARD_INSTALL,
MADV_GUARD_REMOVE,
] {
assert_eq!(madvise_action(advice), MadviseAction::ForwardSemantic);
}
for advice in [
libc::MADV_FREE,
libc::MADV_MERGEABLE,
libc::MADV_UNMERGEABLE,
libc::MADV_HUGEPAGE,
libc::MADV_NOHUGEPAGE,
libc::MADV_COLD,
libc::MADV_PAGEOUT,
] {
assert_eq!(madvise_action(advice), MadviseAction::Ignore);
}
for advice in [
libc::MADV_REMOVE,
libc::MADV_POPULATE_READ,
libc::MADV_POPULATE_WRITE,
libc::MADV_COLLAPSE,
] {
assert_eq!(madvise_action(advice), MadviseAction::Reject(Errno::EINVAL));
}
for advice in [libc::MADV_HWPOISON, libc::MADV_SOFT_OFFLINE] {
assert_eq!(madvise_action(advice), MadviseAction::Reject(Errno::EPERM));
}
assert_eq!(madvise_action(i32::MAX), MadviseAction::Unknown);
}
#[test]
fn common_argument_validation_is_host_independent() {
let aligned = unsafe { AddrMut::<libc::c_void>::from_raw_unchecked(0x1000) };
assert!(
validate_common_args(
syscalls::Madvise::new()
.with_addr(Some(aligned))
.with_len(0)
.with_advice(libc::MADV_FREE),
)
.is_ok()
);
let unaligned = unsafe { AddrMut::<libc::c_void>::from_raw_unchecked(0x1001) };
assert!(
validate_common_args(
syscalls::Madvise::new()
.with_addr(Some(unaligned))
.with_len(0)
.with_advice(libc::MADV_FREE),
)
.is_err()
);
let near_end =
unsafe { AddrMut::<libc::c_void>::from_raw_unchecked(usize::MAX & !(PAGE_SIZE - 1)) };
assert!(
validate_common_args(
syscalls::Madvise::new()
.with_addr(Some(near_end))
.with_len(PAGE_SIZE)
.with_advice(libc::MADV_FREE),
)
.is_err()
);
}
}