Skip to content

Commit ad6a4cc

Browse files
committed
Started to add wrapper for bubblewrap that I want to use to make my integration tests without a heavy container engine as docker. IPC with the client application is not working, yet.
1 parent 61cfb84 commit ad6a4cc

6 files changed

Lines changed: 291 additions & 4 deletions

File tree

docs/TESTS.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
## Integration tests
44

5-
Run with `cargo test -p vuinputd-tests --features "requires-root requires-uinput"`.
5+
Install bubblewrap:
6+
`apt-get install bubblewrap`.
7+
8+
Run with `cargo test -p vuinputd-tests --features "requires-root requires-uinput requires-bwrap"`.
69

710
## Manual end-to-end tests
811

vuinputd-tests/Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ edition = "2021"
66
[[bin]]
77
name = "keyboard-in-container"
88

9+
[[bin]]
10+
name = "bwrap-ipc"
11+
912
[dependencies]
1013
uinput-ioctls = { path = "../uinput-ioctls" }
11-
nix = { version = "0.30", features = ["ioctl"] } # ioctl & libc bindings
14+
nix = { version = "0.30", features = ["ioctl","socket"] } # ioctl & libc bindings
1215
libc = "0.2" # raw system calls
1316
libudev = "0.3" # enumerate-udev
1417

1518
[features]
1619
requires-root = []
17-
requires-uinput = []
20+
requires-uinput = []
21+
requires-bwrap = []
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use core::panic;
2+
use std::{str::from_utf8_unchecked, time::Duration};
3+
4+
use vuinputd_tests::bwrap::SandboxChildIpc;
5+
6+
fn main() {
7+
println!("starting bwrap-ipc");
8+
let ipc = unsafe { SandboxChildIpc::from_fd() };
9+
10+
let incoming = ipc
11+
.recv(Some(Duration::from_secs(5)))
12+
.expect("error receiving input from ipc as child within 5 seconds");
13+
let incoming_str =
14+
str::from_utf8(&incoming).expect("message received from ipc is not encoded as utf8");
15+
if incoming_str == "continue" {
16+
ipc.send(b"ok").unwrap();
17+
} else {
18+
ipc.send(b"nok").unwrap();
19+
panic!("expected ipc message to be 'continue'");
20+
}
21+
}

vuinputd-tests/src/bwrap.rs

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// SPDX-License-Identifier: MIT
2+
//
3+
// Author: Johannes Leupolz <dev@leupolz.eu>
4+
5+
use std::io;
6+
use std::os::fd::{FromRawFd, IntoRawFd, RawFd};
7+
use std::os::unix::net::UnixDatagram;
8+
use std::os::unix::process::CommandExt;
9+
use std::process::{Command, Output};
10+
use std::time::Duration;
11+
12+
use nix::errno::Errno;
13+
use nix::sys::socket::{socketpair, AddressFamily, SockFlag, SockType};
14+
use nix::unistd::close;
15+
16+
/// Check if bubblewrap is available.
17+
pub fn bwrap_available() -> bool {
18+
Command::new("bwrap")
19+
.arg("--version")
20+
.output()
21+
.map(|o| o.status.success())
22+
.unwrap_or(false)
23+
}
24+
25+
/// IPC handle kept by the parent.
26+
pub struct SandboxIpc {
27+
sock: UnixDatagram,
28+
}
29+
30+
impl SandboxIpc {
31+
pub fn recv(&self, read_timeout: Option<Duration>) -> io::Result<Vec<u8>> {
32+
let mut buf = vec![0u8; 4096];
33+
self.sock.set_read_timeout(read_timeout)?;
34+
let n = self.sock.recv(&mut buf)?;
35+
buf.truncate(n);
36+
Ok(buf)
37+
}
38+
39+
pub fn send(&self, data: &[u8]) -> io::Result<()> {
40+
self.sock.send(data)?;
41+
Ok(())
42+
}
43+
}
44+
45+
/// IPC handle inside the container.
46+
pub struct SandboxChildIpc {
47+
sock: UnixDatagram,
48+
}
49+
50+
impl SandboxChildIpc {
51+
/// FD number is fixed and known.
52+
pub const FD: RawFd = 3;
53+
54+
/// # Safety
55+
/// Must only be called once in the child.
56+
pub unsafe fn from_fd() -> Self {
57+
let sock = UnixDatagram::from_raw_fd(Self::FD);
58+
Self { sock }
59+
}
60+
61+
pub fn send(&self, data: &[u8]) -> io::Result<()> {
62+
self.sock.send(data)?;
63+
Ok(())
64+
}
65+
66+
pub fn recv(&self, read_timeout: Option<Duration>) -> io::Result<Vec<u8>> {
67+
let mut buf = vec![0u8; 4096];
68+
self.sock.set_read_timeout(read_timeout)?;
69+
let n = self.sock.recv(&mut buf)?;
70+
buf.truncate(n);
71+
Ok(buf)
72+
}
73+
}
74+
75+
/// Builder for bubblewrap invocations.
76+
#[derive(Default)]
77+
pub struct BwrapBuilder {
78+
args: Vec<String>,
79+
ipc_child_fd: Option<RawFd>,
80+
}
81+
82+
impl BwrapBuilder {
83+
pub fn new() -> Self {
84+
Self::default()
85+
}
86+
87+
pub fn unshare_all(mut self) -> Self {
88+
self.args.push("--unshare-all".into());
89+
self
90+
}
91+
92+
pub fn unshare_net(mut self) -> Self {
93+
self.args.push("--unshare-net".into());
94+
self
95+
}
96+
97+
pub fn proc(mut self) -> Self {
98+
self.args.push("--proc".into());
99+
self.args.push("/proc".into());
100+
self
101+
}
102+
103+
pub fn tmpfs(mut self, path: &str) -> Self {
104+
self.args.push("--tmpfs".into());
105+
self.args.push(path.into());
106+
self
107+
}
108+
109+
// https://superuser.com/questions/1577262/bwrap-execvp-no-such-file-or-directory-when-ro-binding-non-root-path
110+
pub fn ro_bind(mut self, src: &str, dst: &str) -> Self {
111+
self.args
112+
.extend(["--ro-bind".into(), src.into(), dst.into()]);
113+
self
114+
}
115+
116+
pub fn bind(mut self, src: &str, dst: &str) -> Self {
117+
self.args.extend(["--bind".into(), src.into(), dst.into()]);
118+
self
119+
}
120+
121+
/// Ensure the container dies if the parent dies.
122+
///
123+
/// This uses bwrap's `--die-with-parent` flag, which internally
124+
/// uses a parent-death signal (PR_SET_PDEATHSIG).
125+
pub fn die_with_parent(mut self) -> Self {
126+
self.args.push("--die-with-parent".into());
127+
self
128+
}
129+
130+
/// Enable bidirectional IPC using a Unix seqpacket socketpair.
131+
pub fn with_ipc(mut self) -> io::Result<(Self, SandboxIpc)> {
132+
let (parent, child) = socketpair(
133+
AddressFamily::Unix,
134+
SockType::SeqPacket,
135+
None,
136+
SockFlag::empty(),
137+
)
138+
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
139+
140+
// Parent side
141+
let parent_sock = unsafe { UnixDatagram::from_raw_fd(parent.into_raw_fd()) };
142+
143+
// Child side must become FD 3 inside container
144+
self.ipc_child_fd = Some(child.into_raw_fd());
145+
146+
Ok((self, SandboxIpc { sock: parent_sock }))
147+
}
148+
149+
/// Final command executed inside the container.
150+
pub fn command(mut self, cmd: &str) -> Self {
151+
//self.args.push("--".into());
152+
self.args.push(cmd.into());
153+
self
154+
}
155+
156+
pub fn run(mut self) -> io::Result<Output> {
157+
println!("Arguments for bwrap: {:?}", &self.args);
158+
159+
let mut cmd = Command::new("bwrap");
160+
161+
if let Some(fd) = self.ipc_child_fd.take() {
162+
// Move child FD to 3. Note that the FD 3 needs to be linked at the
163+
// beginning of the child program.
164+
unsafe {
165+
cmd.pre_exec(move || {
166+
let res = libc::dup2(fd, SandboxChildIpc::FD);
167+
Errno::result(res)
168+
.map(drop)
169+
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
170+
close(fd).ok();
171+
Ok(())
172+
})
173+
};
174+
}
175+
176+
cmd.args(&self.args).output()
177+
}
178+
}
179+
180+
#[cfg(feature = "requires-bwrap")]
181+
#[cfg(test)]
182+
mod tests {
183+
use super::*;
184+
185+
#[test]
186+
fn bwrap_works() {
187+
if !bwrap_available() {
188+
panic!("bwrap not available");
189+
}
190+
191+
let out = BwrapBuilder::new()
192+
.unshare_net()
193+
//.proc()
194+
.ro_bind("/", "/")
195+
.tmpfs("/tmp")
196+
.die_with_parent()
197+
.command("/usr/bin/sh")
198+
.run()
199+
.unwrap_or_else(|e| panic!("failed to run bwrap!: {e}"));
200+
201+
println!("Output");
202+
println!("stdout: {}", str::from_utf8(&out.stdout).unwrap());
203+
println!("stderr: {}", str::from_utf8(&out.stderr).unwrap());
204+
205+
assert!(out.status.success());
206+
}
207+
}

vuinputd-tests/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod bwrap;

vuinputd-tests/tests/integration_tests.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,55 @@
1-
use std::process::Command;
1+
// SPDX-License-Identifier: MIT
2+
//
3+
// Author: Johannes Leupolz <dev@leupolz.eu>
4+
5+
use std::{process::Command, time::Duration};
6+
use vuinputd_tests::bwrap;
7+
8+
#[cfg(all(feature = "requires-root", feature = "requires-bwrap"))]
9+
#[test]
10+
fn test_bwrap_simple() {
11+
let out = bwrap::BwrapBuilder::new()
12+
.unshare_all()
13+
.ro_bind("/", "/")
14+
.tmpfs("/tmp")
15+
.die_with_parent()
16+
.command("ls /")
17+
.run()
18+
.unwrap_or_else(|e| panic!("failed to run bwrap!: {e}"));
19+
20+
println!("Output");
21+
println!("stdout: {}", str::from_utf8(&out.stdout).unwrap());
22+
println!("stderr: {}", str::from_utf8(&out.stderr).unwrap());
23+
}
24+
25+
#[cfg(all(feature = "requires-root", feature = "requires-bwrap"))]
26+
#[ignore]
27+
#[test]
28+
fn test_bwrap_ipc() {
29+
let bwrap_ipc = env!("CARGO_BIN_EXE_bwrap-ipc");
30+
31+
let (builder, ipc) = bwrap::BwrapBuilder::new()
32+
.unshare_all()
33+
.ro_bind("/", "/")
34+
.tmpfs("/tmp")
35+
.die_with_parent()
36+
.with_ipc()
37+
.expect("failed to create IPC");
38+
39+
let out = builder
40+
.command(bwrap_ipc)
41+
.run()
42+
.unwrap_or_else(|e| panic!("failed to run bwrap!: {e}"));
43+
ipc.send("continue".as_bytes())
44+
.unwrap_or_else(|e| panic!("failed to send data via ipc: {e}"));
45+
46+
ipc.recv(Some(Duration::from_secs(5)))
47+
.expect("error receiving input from ipc as host within 5 seconds");
48+
49+
println!("Output");
50+
println!("stdout: {}", str::from_utf8(&out.stdout).unwrap());
51+
println!("stderr: {}", str::from_utf8(&out.stderr).unwrap());
52+
}
253

354
#[cfg(all(feature = "requires-root", feature = "requires-uinput"))]
455
#[test]

0 commit comments

Comments
 (0)