Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 0 additions & 71 deletions .github/workflows/ci.yml

This file was deleted.

12 changes: 4 additions & 8 deletions .github/workflows/main-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,9 @@ jobs:
steps:
- name: Pre-checkout cleanup
run: |
for item in outputs target .git/index.lock; do
if [ -d "$item" ] || [ -f "$item" ]; then
docker run --rm -v "$(pwd):/workspace" busybox:1.36.1 sh -c \
"rm -rf /workspace/$item" 2>/dev/null || \
sudo rm -rf "$item" 2>/dev/null || true
fi
done
docker run --rm -v "$(pwd):/workspace" busybox:1.36.1 sh -c \
"rm -rf /workspace/outputs /workspace/.git/index.lock" 2>/dev/null || \
sudo rm -rf outputs .git/index.lock 2>/dev/null || true

- name: Checkout
uses: actions/checkout@v4
Expand All @@ -54,7 +50,7 @@ jobs:

# -- Tests -------------------------------------------------------------
- name: Test
run: docker compose --profile ci run --rm rust-ci cargo test
run: docker compose --profile ci run --rm rust-ci cargo test -- --test-threads=4

# -- Build -------------------------------------------------------------
- name: Build
Expand Down
12 changes: 4 additions & 8 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,9 @@ jobs:
steps:
- name: Pre-checkout cleanup
run: |
for item in outputs target .git/index.lock; do
if [ -d "$item" ] || [ -f "$item" ]; then
docker run --rm -v "$(pwd):/workspace" busybox:1.36.1 sh -c \
"rm -rf /workspace/$item" 2>/dev/null || \
sudo rm -rf "$item" 2>/dev/null || true
fi
done
docker run --rm -v "$(pwd):/workspace" busybox:1.36.1 sh -c \
"rm -rf /workspace/outputs /workspace/.git/index.lock" 2>/dev/null || \
sudo rm -rf outputs .git/index.lock 2>/dev/null || true

- name: Checkout
uses: actions/checkout@v4
Expand All @@ -71,7 +67,7 @@ jobs:

# -- Tests -------------------------------------------------------------
- name: Test
run: docker compose --profile ci run --rm rust-ci cargo test
run: docker compose --profile ci run --rm rust-ci cargo test -- --test-threads=4

# -- Build -------------------------------------------------------------
- name: Build
Expand Down
7 changes: 4 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ repos:
args: [--unsafe]
- id: check-added-large-files
args: [--maxkb=1000]
exclude: '^Cargo\.lock$'
- id: check-json
- id: pretty-format-json
args: [--autofix, --no-sort-keys]
Expand All @@ -35,19 +36,19 @@ repos:
types: [shell]
args: [-x]

# Rust formatting and linting (containerized)
# Rust formatting and linting (local, no Docker rebuild)
- repo: local
hooks:
- id: rust-fmt
name: Rust format check
entry: docker compose --profile ci run --rm rust-ci cargo fmt --all -- --check
entry: cargo fmt --all -- --check
language: system
files: '\.rs$'
pass_filenames: false

- id: rust-clippy
name: Rust clippy lint
entry: docker compose --profile ci run --rm rust-ci cargo clippy --all-targets -- -D warnings
entry: cargo clippy --all-targets -- -D warnings
language: system
files: '\.rs$'
pass_filenames: false
14 changes: 14 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ authors = ["AndrewAltimit"]
[workspace.dependencies]
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
bitcode = { version = "0.6", features = ["serde"] }
bincode = "1.3" # Legacy: only used in itk-net (laminar dependency)

Expand Down Expand Up @@ -89,6 +90,17 @@ parking_lot = "0.12"
# Vulkan
ash = { version = "0.38", features = ["loaded"] }

# CLI
clap = { version = "4", features = ["derive"] }

# GUI
egui = "0.28"
egui-wgpu = "0.28"
egui-winit = "0.28"
wgpu = "0.20"
winit = "0.29"
once_cell = "1.19"

# Hooking
retour = { version = "0.3", features = ["static-detour"] }

Expand Down Expand Up @@ -125,6 +137,8 @@ clone_on_ref_ptr = "warn"
dbg_macro = "deny"
todo = "deny"
unimplemented = "deny"
undocumented_unsafe_blocks = "warn"
missing_safety_doc = "warn"

[workspace.lints.rust]
unsafe_op_in_unsafe_fn = "warn"
57 changes: 24 additions & 33 deletions core/itk-ipc/src/unix_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,16 @@ fn remove_socket_file(path: &str) {

/// Non-blocking receive helper that keeps the lock held while consuming data.
///
/// This prevents race conditions where another thread could consume data between
/// peeking and actually reading.
/// Uses a single MSG_PEEK to check for the header, then a single blocking recv
/// to consume the full message. This avoids the race condition where data could
/// arrive between multiple peek operations.
fn try_recv_with_fd(fd: std::os::unix::io::RawFd) -> Result<Option<Vec<u8>>> {
use itk_protocol::HEADER_SIZE;

// Use MSG_PEEK to check if enough data is available without consuming bytes.
// Use MSG_PEEK to check if enough data is available for the header.
let mut peek_buf = [0u8; HEADER_SIZE];
// SAFETY: `fd` is a valid socket file descriptor owned by the caller.
// `peek_buf` is a stack-allocated buffer with known size HEADER_SIZE.
let peeked = unsafe {
libc::recv(
fd,
Expand Down Expand Up @@ -96,55 +99,40 @@ fn try_recv_with_fd(fd: std::os::unix::io::RawFd) -> Result<Option<Vec<u8>>> {
let header = itk_protocol::Header::from_bytes(&peek_buf).map_err(IpcError::Protocol)?;
let total_size = HEADER_SIZE + header.payload_len as usize;

// Peek again to check if full message is available
// Allocate buffer for the full message based on header-parsed length
let mut message = vec![0u8; total_size];
let peeked_full = unsafe {
libc::recv(
fd,
message.as_mut_ptr() as *mut libc::c_void,
total_size,
libc::MSG_PEEK | libc::MSG_DONTWAIT,
)
};

if peeked_full < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::WouldBlock
|| err.kind() == std::io::ErrorKind::Interrupted
{
return Ok(None);
}
return Err(IpcError::Io(err));
}

if (peeked_full as usize) < total_size {
return Ok(None);
}

// Full message is available - consume it (we still hold the lock in the caller)
// Single recv to consume the full message (we already know the header is available,
// and stream sockets guarantee payload follows header in order)
// SAFETY: `fd` is a valid socket file descriptor. `message` is a heap-allocated
// buffer of `total_size` bytes, matching the length passed to recv.
let received = unsafe {
libc::recv(
fd,
message.as_mut_ptr() as *mut libc::c_void,
total_size,
0, // Blocking read, but we know data is available
libc::MSG_DONTWAIT,
)
};

if received < 0 {
let err = std::io::Error::last_os_error();
// EINTR on final recv is unusual but handle it by reporting no data available
if err.kind() == std::io::ErrorKind::Interrupted {
if err.kind() == std::io::ErrorKind::WouldBlock
|| err.kind() == std::io::ErrorKind::Interrupted
{
return Ok(None);
}
return Err(IpcError::Io(err));
}

if (received as usize) != total_size {
if (received as usize) < total_size {
// Partial message: payload not fully available yet.
// Since we already consumed partial data from the stream, we can't
// put it back. This indicates a genuine protocol framing issue.
return Err(IpcError::Protocol(
itk_protocol::ProtocolError::IncompletePayload {
need: total_size - itk_protocol::HEADER_SIZE,
have: (received as usize).saturating_sub(itk_protocol::HEADER_SIZE),
need: total_size - HEADER_SIZE,
have: (received as usize).saturating_sub(HEADER_SIZE),
},
));
}
Expand Down Expand Up @@ -250,11 +238,14 @@ impl UnixSocketServer {
// Set restrictive umask before creating socket to prevent race condition.
// Without this, the socket would briefly exist with default permissions
// (potentially allowing other users to connect) before set_permissions runs.
// SAFETY: `libc::umask` is always safe to call; it atomically sets the
// process umask and returns the previous value.
let old_umask = unsafe { libc::umask(0o077) };

let bind_result = UnixListener::bind(&path);

// Restore original umask immediately after bind
// SAFETY: Restoring the previously-saved umask value; always safe.
unsafe {
libc::umask(old_umask);
}
Expand Down
Loading