Skip to content

Commit

Permalink
Add detours for fcntl/dup system calls (#115)
Browse files Browse the repository at this point in the history
* Add detours for fcntl/dup syscalls
  • Loading branch information
infiniteregrets authored Jun 8, 2022
1 parent bb75903 commit 2207fdc
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 154 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ jobs:
- run: |
sudo apt-get update -y
sudo apt-get install -y libpcap-dev cmake
- uses: actions/setup-python@v3
- run: pip3 install flask
- name: start minikube
uses: medyagh/setup-minikube@master
with:
Expand All @@ -143,7 +145,7 @@ jobs:
- run: minikube image load test:latest
- name: setup nginx
run: kubectl apply -f tests/app.yaml
- name: run node tests
- name: cargo test
run: cargo test --package tests --lib -- tests --nocapture --test-threads 1
- name: switch minikube runtime
run: |
Expand Down
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how

## [Unreleased]
### Added
- File operations are now available behind the `MIRRORD_FILE_OPS` env variable, this means that mirrord now hooks into the following file functions: `open`, `fopen`, `fdopen`, `openat`, `read`, `fread`, `fileno`, `lseek`, and `write` to provide a mirrored file system.
- Support for running x64 (Intel) binary on arm (Silicon) macOS using mirrord. This will download and use the x64 mirrord-layer binary when needed.
- Add detours for fcntl/dup system calls, closes [#51](https://github.com/metalbear-co/mirrord/issues/51)

### Changed
- Added graceful exit for library extraction logic in case of error
- Add graceful exit for library extraction logic in case of error.
- Refactor the CI by splitting the building of mirrord-agent in a separate job and caching the agent image for E2E tests.
- File operations are now available behind the `MIRRORD_FILE_OPS` env variable, this means that mirrord now hooks into the following file functions: `open`, `fopen`, `fdopen`, `openat`, `read`, `fread`, `fileno`, `lseek`, and `write` to provide a mirrored file system.
- Update bug report template to apply to the latest version of mirrord.
- Changed release profile to strip debuginfo and enable LTO.
- Chang release profile to strip debuginfo and enable LTO.
- VS Code extension - update dependencies.

### Fixed
Expand Down
18 changes: 16 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion mirrord-layer/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![feature(c_variadic)]
#![feature(once_cell)]
#![feature(result_option_inspect)]
#![feature(const_trait_impl)]
Expand Down Expand Up @@ -542,7 +543,7 @@ unsafe extern "C" fn close_detour(fd: c_int) -> c_int {
.get()
.expect("Should be set during initialization!");

if SOCKETS.lock().unwrap().remove(&fd) {
if SOCKETS.lock().unwrap().remove(&fd).is_some() {
libc::close(fd)
} else if *enabled_file_ops {
let remote_fd = OPEN_FILES.lock().unwrap().remove(&fd);
Expand Down
137 changes: 82 additions & 55 deletions mirrord-layer/src/sockets.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
//! We implement each hook function in a safe function as much as possible, having the unsafe do the
//! absolute minimum
use std::{
borrow::Borrow,
collections::{HashMap, HashSet, VecDeque},
hash::{Hash, Hasher},
collections::{HashMap, VecDeque},
lazy::SyncLazy,
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
os::unix::io::RawFd,
sync::Mutex,
sync::{Arc, Mutex},
};

use errno::{errno, set_errno, Errno};
Expand All @@ -22,8 +20,8 @@ use crate::{
HOOK_SENDER,
};

pub(crate) static SOCKETS: SyncLazy<Mutex<HashSet<Socket>>> =
SyncLazy::new(|| Mutex::new(HashSet::new()));
pub(crate) static SOCKETS: SyncLazy<Mutex<HashMap<RawFd, Arc<Socket>>>> =
SyncLazy::new(|| Mutex::new(HashMap::new()));

pub static CONNECTION_QUEUE: SyncLazy<Mutex<ConnectionQueue>> =
SyncLazy::new(|| Mutex::new(ConnectionQueue::default()));
Expand Down Expand Up @@ -75,7 +73,7 @@ pub struct Connected {
local_address: SocketAddr,
}

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
pub struct Bound {
address: SocketAddr,
}
Expand All @@ -96,34 +94,13 @@ impl Default for SocketState {

#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct Socket {
fd: RawFd,
pub struct Socket {
domain: c_int,
type_: c_int,
protocol: c_int,
pub state: SocketState,
}

impl PartialEq for Socket {
fn eq(&self, other: &Self) -> bool {
self.fd == other.fd
}
}

impl Eq for Socket {}

impl Hash for Socket {
fn hash<H: Hasher>(&self, state: &mut H) {
self.fd.hash(state);
}
}

impl Borrow<RawFd> for Socket {
fn borrow(&self) -> &RawFd {
&self.fd
}
}

#[inline]
fn is_ignored_port(port: Port) -> bool {
port == 0 || (port > 50000 && port < 60000)
Expand All @@ -144,13 +121,15 @@ fn socket(domain: c_int, type_: c_int, protocol: c_int) -> RawFd {
return fd;
}
let mut sockets = SOCKETS.lock().unwrap();
sockets.insert(Socket {
sockets.insert(
fd,
domain,
type_,
protocol,
state: SocketState::default(),
});
Arc::new(Socket {
domain,
type_,
protocol,
state: SocketState::default(),
}),
);
fd
}

Expand All @@ -167,7 +146,7 @@ fn bind(sockfd: c_int, addr: *const sockaddr, addrlen: socklen_t) -> c_int {
debug!("bind called sockfd: {:?}", sockfd);
let mut socket = {
let mut sockets = SOCKETS.lock().unwrap();
match sockets.take(&sockfd) {
match sockets.remove(&sockfd) {
Some(socket) if !matches!(socket.state, SocketState::Initialized) => {
error!("socket is in invalid state for bind {:?}", socket.state);
return libc::EINVAL;
Expand Down Expand Up @@ -195,12 +174,12 @@ fn bind(sockfd: c_int, addr: *const sockaddr, addrlen: socklen_t) -> c_int {
return unsafe { libc::bind(sockfd, addr, addrlen) };
}

socket.state = SocketState::Bound(Bound {
Arc::get_mut(&mut socket).unwrap().state = SocketState::Bound(Bound {
address: parsed_addr,
});

let mut sockets = SOCKETS.lock().unwrap();
sockets.insert(socket);
sockets.insert(sockfd, socket);
0
}

Expand All @@ -220,18 +199,18 @@ fn listen(sockfd: RawFd, _backlog: c_int) -> c_int {
debug!("listen called");
let mut socket = {
let mut sockets = SOCKETS.lock().unwrap();
match sockets.take(&sockfd) {
match sockets.remove(&sockfd) {
Some(socket) => socket,
None => {
debug!("listen: no socket found for fd: {}", &sockfd);
return unsafe { libc::listen(sockfd, _backlog) };
}
}
};
match socket.state {
match &socket.state {
SocketState::Bound(bound) => {
let real_port = bound.address.port();
socket.state = SocketState::Listening(bound);
Arc::get_mut(&mut socket).unwrap().state = SocketState::Listening(*bound);
let mut os_addr = match socket.domain {
libc::AF_INET => {
OsSocketAddr::from(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0))
Expand Down Expand Up @@ -304,7 +283,7 @@ fn listen(sockfd: RawFd, _backlog: c_int) -> c_int {
}
debug!("listen: success");
let mut sockets = SOCKETS.lock().unwrap();
sockets.insert(socket);
sockets.insert(sockfd, socket);
0
}

Expand All @@ -319,7 +298,7 @@ fn connect(sockfd: RawFd, address: *const sockaddr, len: socklen_t) -> c_int {

let socket = {
let mut sockets = SOCKETS.lock().unwrap();
match sockets.take(&sockfd) {
match sockets.remove(&sockfd) {
Some(socket) => socket,
None => {
debug!("connect: no socket found for fd: {}", &sockfd);
Expand All @@ -329,7 +308,7 @@ fn connect(sockfd: RawFd, address: *const sockaddr, len: socklen_t) -> c_int {
};

// We don't handle this socket, so restore state if there was any. (delay execute bind)
if let SocketState::Bound(bound) = socket.state {
if let SocketState::Bound(bound) = &socket.state {
let os_addr = OsSocketAddr::from(bound.address);
let ret = unsafe { libc::bind(sockfd, os_addr.as_ptr(), os_addr.len()) };
if ret != 0 {
Expand Down Expand Up @@ -459,16 +438,10 @@ fn accept(
address_len: *mut socklen_t,
new_fd: RawFd,
) -> RawFd {
let (origin_fd, local_address, domain, protocol, type_) = {
let (local_address, domain, protocol, type_) = {
if let Some(socket) = SOCKETS.lock().unwrap().get(&sockfd) {
if let SocketState::Listening(bound) = &socket.state {
(
socket.fd,
bound.address,
socket.domain,
socket.protocol,
socket.type_,
)
(bound.address, socket.domain, socket.protocol, socket.type_)
} else {
error!("original socket is not listening");
return new_fd;
Expand All @@ -478,7 +451,7 @@ fn accept(
return new_fd;
}
};
let socket_info = { CONNECTION_QUEUE.lock().unwrap().get(&origin_fd) };
let socket_info = { CONNECTION_QUEUE.lock().unwrap().get(&sockfd) };
let remote_address = match socket_info {
Some(socket_info) => socket_info,
None => {
Expand All @@ -488,7 +461,6 @@ fn accept(
}
.address;
let new_socket = Socket {
fd: new_fd,
domain,
protocol,
type_,
Expand All @@ -499,7 +471,7 @@ fn accept(
};
fill_address(address, address_len, remote_address);

SOCKETS.lock().unwrap().insert(new_socket);
SOCKETS.lock().unwrap().insert(new_fd, Arc::new(new_socket));
new_fd
}

Expand Down Expand Up @@ -533,17 +505,72 @@ unsafe extern "C" fn accept4_detour(
}
}

fn fcntl(orig_fd: c_int, cmd: c_int, fcntl_fd: i32) -> c_int {
if fcntl_fd == -1 {
error!("fcntl failed");
return fcntl_fd;
}
match cmd {
libc::F_DUPFD | libc::F_DUPFD_CLOEXEC => {
dup(orig_fd, fcntl_fd);
}
_ => (),
}
fcntl_fd
}

unsafe extern "C" fn fcntl_detour(fd: c_int, cmd: c_int, arg: ...) -> c_int {
let fcntl_fd = libc::fcntl(fd, cmd, arg);
fcntl(fd, cmd, fcntl_fd)
}

fn dup(fd: c_int, dup_fd: i32) -> c_int {
if dup_fd == -1 {
error!("dup failed");
return dup_fd;
}
let mut sockets = SOCKETS.lock().unwrap();
if let Some(socket) = sockets.get(&fd) {
let dup_socket = socket.clone();
sockets.insert(dup_fd as RawFd, dup_socket);
}
dup_fd
}

unsafe extern "C" fn dup_detour(fd: c_int) -> c_int {
let dup_fd = libc::dup(fd);
dup(fd, dup_fd)
}

unsafe extern "C" fn dup2_detour(oldfd: c_int, newfd: c_int) -> c_int {
if oldfd == newfd {
return newfd;
}
let dup2_fd = libc::dup2(oldfd, newfd);
dup(oldfd, dup2_fd)
}

#[cfg(target_os = "linux")]
unsafe extern "C" fn dup3_detour(oldfd: c_int, newfd: c_int, flags: c_int) -> c_int {
let dup3_fd = libc::dup3(oldfd, newfd, flags);
dup(oldfd, dup3_fd)
}

pub fn enable_socket_hooks(interceptor: &mut Interceptor) {
hook!(interceptor, "socket", socket_detour);
hook!(interceptor, "bind", bind_detour);
hook!(interceptor, "listen", listen_detour);
hook!(interceptor, "connect", connect_detour);
hook!(interceptor, "fcntl", fcntl_detour);
hook!(interceptor, "dup", dup_detour);
hook!(interceptor, "dup2", dup2_detour);
try_hook!(interceptor, "getpeername", getpeername_detour);
try_hook!(interceptor, "getsockname", getsockname_detour);
#[cfg(target_os = "linux")]
{
try_hook!(interceptor, "uv__accept4", accept4_detour);
try_hook!(interceptor, "accept4", accept4_detour);
try_hook!(interceptor, "dup3", dup3_detour);
}
try_hook!(interceptor, "accept", accept_detour);
}
4 changes: 3 additions & 1 deletion tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ tokio = { version = "1", features = ["macros"] }
serde_json = "1.0.79"
mirrord = { artifact = "bin", bin = true, path = "../mirrord-cli" }
serde = "1.0.137"
futures = "0.3.21"
futures = "0.3.21"
lazy_static = "1.4.0"
nix = "0.24.1"
Loading

0 comments on commit 2207fdc

Please sign in to comment.