Skip to content

Commit c79d1bf

Browse files
authored
Abandon contract after running command in zone (#3761)
1 parent 385ff29 commit c79d1bf

File tree

3 files changed

+150
-14
lines changed

3 files changed

+150
-14
lines changed

illumos-utils/src/lib.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,33 @@ mod inner {
9090
}))
9191
}
9292

93-
// Helper function for starting the process and checking the
93+
// Helper functions for starting the process and checking the
9494
// exit code result.
95+
96+
pub fn spawn(
97+
command: &mut std::process::Command,
98+
) -> Result<std::process::Child, ExecutionError> {
99+
command.spawn().map_err(|err| ExecutionError::ExecutionStart {
100+
command: to_string(command),
101+
err,
102+
})
103+
}
104+
105+
pub fn run_child(
106+
command: &mut std::process::Command,
107+
child: std::process::Child,
108+
) -> Result<std::process::Output, ExecutionError> {
109+
let output = child.wait_with_output().map_err(|err| {
110+
ExecutionError::ExecutionStart { command: to_string(command), err }
111+
})?;
112+
113+
if !output.status.success() {
114+
return Err(output_to_exec_error(command, &output));
115+
}
116+
117+
Ok(output)
118+
}
119+
95120
pub fn execute(
96121
command: &mut std::process::Command,
97122
) -> Result<std::process::Output, ExecutionError> {

illumos-utils/src/running_zone.rs

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ use crate::svc::wait_for_service;
1212
use crate::zone::{AddressRequest, IPADM, ZONE_PREFIX};
1313
use camino::{Utf8Path, Utf8PathBuf};
1414
use ipnetwork::IpNetwork;
15+
#[cfg(target_os = "illumos")]
16+
use libc::pid_t;
1517
use omicron_common::backoff;
1618
use slog::error;
1719
use slog::info;
@@ -156,10 +158,15 @@ pub enum GetZoneError {
156158
// inside a non-global zone.
157159
#[cfg(target_os = "illumos")]
158160
mod zenter {
161+
use libc::ctid_t;
162+
use libc::pid_t;
159163
use libc::zoneid_t;
160164
use std::ffi::c_int;
161165
use std::ffi::c_uint;
162-
use std::ffi::CStr;
166+
use std::ffi::{CStr, CString};
167+
use std::fs::File;
168+
use std::os::unix::prelude::FileExt;
169+
use std::process;
163170

164171
#[link(name = "contract")]
165172
extern "C" {
@@ -169,13 +176,68 @@ mod zenter {
169176
fn ct_pr_tmpl_set_param(fd: c_int, params: c_uint) -> c_int;
170177
fn ct_tmpl_activate(fd: c_int) -> c_int;
171178
fn ct_tmpl_clear(fd: c_int) -> c_int;
179+
fn ct_ctl_abandon(fd: c_int) -> c_int;
172180
}
173181

174182
#[link(name = "c")]
175183
extern "C" {
176184
pub fn zone_enter(zid: zoneid_t) -> c_int;
177185
}
178186

187+
#[derive(thiserror::Error, Debug)]
188+
pub enum AbandonContractError {
189+
#[error("Error opening file {file}: {error}")]
190+
Open { file: String, error: std::io::Error },
191+
192+
#[error("Error abandoning contract {ctid}: {error}")]
193+
Abandon { ctid: ctid_t, error: std::io::Error },
194+
195+
#[error("Error closing file {file}: {error}")]
196+
Close { file: String, error: std::io::Error },
197+
}
198+
199+
pub fn get_contract(pid: pid_t) -> std::io::Result<ctid_t> {
200+
// The offset of "id_t pr_contract" in struct psinfo which is an
201+
// interface documented in proc(5).
202+
const CONTRACT_OFFSET: u64 = 280;
203+
204+
let path = format!("/proc/{}/psinfo", pid);
205+
206+
let file = File::open(path)?;
207+
let mut buffer = [0; 4];
208+
file.read_exact_at(&mut buffer, CONTRACT_OFFSET)?;
209+
Ok(ctid_t::from_ne_bytes(buffer))
210+
}
211+
212+
pub fn abandon_contract(ctid: ctid_t) -> Result<(), AbandonContractError> {
213+
let path = format!("/proc/{}/contracts/{}/ctl", process::id(), ctid);
214+
215+
let cpath = CString::new(path.clone()).unwrap();
216+
let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_WRONLY) };
217+
if fd < 0 {
218+
return Err(AbandonContractError::Open {
219+
file: path,
220+
error: std::io::Error::last_os_error(),
221+
});
222+
}
223+
let ret = unsafe { ct_ctl_abandon(fd) };
224+
if ret != 0 {
225+
unsafe { libc::close(fd) };
226+
return Err(AbandonContractError::Abandon {
227+
ctid,
228+
error: std::io::Error::from_raw_os_error(ret),
229+
});
230+
}
231+
if unsafe { libc::close(fd) } != 0 {
232+
return Err(AbandonContractError::Close {
233+
file: path,
234+
error: std::io::Error::last_os_error(),
235+
});
236+
}
237+
238+
Ok(())
239+
}
240+
179241
// A Rust wrapper around the process contract template.
180242
#[derive(Debug)]
181243
pub struct Template {
@@ -277,11 +339,11 @@ impl RunningZone {
277339
// another, if the task is swapped out at an await point. That would leave
278340
// the first thread's template in a modified state.
279341
//
280-
// If we do need to make this method asynchronous, we will need to change the
281-
// internals to avoid changing the thread's contract. One possible approach
282-
// here would be to use `libscf` directly, rather than `exec`-ing `svccfg`
283-
// directly in a forked child. That would obviate the need to work on the
284-
// contract at all.
342+
// If we do need to make this method asynchronous, we will need to change
343+
// the internals to avoid changing the thread's contract. One possible
344+
// approach here would be to use `libscf` directly, rather than `exec`-ing
345+
// `svccfg` directly in a forked child. That would obviate the need to work
346+
// on the contract at all.
285347
#[cfg(target_os = "illumos")]
286348
pub fn run_cmd<I, S>(&self, args: I) -> Result<String, RunCommandError>
287349
where
@@ -319,13 +381,46 @@ impl RunningZone {
319381
}
320382
let command = command.args(args);
321383

322-
// Capture the result, and be sure to clear the template for this
323-
// process itself before returning.
324-
let res = crate::execute(command).map_err(|err| RunCommandError {
384+
let child = crate::spawn(command).map_err(|err| RunCommandError {
325385
zone: self.name().to_string(),
326386
err,
387+
})?;
388+
389+
// Record the process contract now in use by the child; the contract
390+
// just created from the template that we applied to this thread
391+
// moments ago. We need to abandon it once the child is finished
392+
// executing.
393+
// unwrap() safety - child.id() returns u32 but pid_t is i32.
394+
// PID_MAX is 999999 so this will not overflow.
395+
let child_pid: pid_t = child.id().try_into().unwrap();
396+
let contract = zenter::get_contract(child_pid);
397+
398+
// Capture the result, and be sure to clear the template for this
399+
// process itself before returning.
400+
let res = crate::run_child(command, child).map_err(|err| {
401+
RunCommandError { zone: self.name().to_string(), err }
327402
});
328403
template.clear();
404+
405+
// Now abandon the contract that was used for the child.
406+
match contract {
407+
Err(e) => error!(
408+
self.inner.log,
409+
"Could not retrieve contract for pid {}: {}", child_pid, e
410+
),
411+
Ok(ctid) => {
412+
if let Err(e) = zenter::abandon_contract(ctid) {
413+
error!(
414+
self.inner.log,
415+
"Failed to abandon contract {} for pid {}: {}",
416+
ctid,
417+
child_pid,
418+
e
419+
);
420+
}
421+
}
422+
}
423+
329424
res.map(|output| String::from_utf8_lossy(&output.stdout).to_string())
330425
}
331426

@@ -342,7 +437,11 @@ impl RunningZone {
342437
// that's actually run is irrelevant.
343438
let mut command = std::process::Command::new("echo");
344439
let command = command.args(args);
345-
crate::execute(command)
440+
let child = crate::spawn(command).map_err(|err| RunCommandError {
441+
zone: self.name().to_string(),
442+
err,
443+
})?;
444+
crate::run_child(command, child)
346445
.map_err(|err| RunCommandError {
347446
zone: self.name().to_string(),
348447
err,

sled-agent/src/services.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3176,8 +3176,19 @@ mod test {
31763176
wait_ctx.expect().return_once(|_, _| Ok(()));
31773177

31783178
// Import the manifest, enable the service
3179-
let execute_ctx = illumos_utils::execute_context();
3180-
execute_ctx.expect().times(..).returning(|_| {
3179+
let spawn_ctx = illumos_utils::spawn_context();
3180+
spawn_ctx.expect().times(..).returning(|_| {
3181+
std::process::Command::new("/bin/false").spawn().map_err(|err| {
3182+
illumos_utils::ExecutionError::ExecutionStart {
3183+
command: "mock".to_string(),
3184+
err,
3185+
}
3186+
})
3187+
});
3188+
let run_child_ctx = illumos_utils::run_child_context();
3189+
run_child_ctx.expect().times(..).returning(|_, mut child| {
3190+
let _ = child.kill();
3191+
31813192
Ok(std::process::Output {
31823193
status: std::process::ExitStatus::from_raw(0),
31833194
stdout: vec![],
@@ -3192,7 +3203,8 @@ mod test {
31923203
Box::new(id_ctx),
31933204
Box::new(ensure_address_ctx),
31943205
Box::new(wait_ctx),
3195-
Box::new(execute_ctx),
3206+
Box::new(spawn_ctx),
3207+
Box::new(run_child_ctx),
31963208
]
31973209
}
31983210

0 commit comments

Comments
 (0)