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
292 changes: 292 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,283 @@ jobs:
cd playground
timeout 60 hemlock test/test_examples.hml || echo "Tests completed"

secure-mode:
runs-on: ubuntu-latest
name: Secure Mode (Bubblewrap)

steps:
- name: Checkout playground
uses: actions/checkout@v4
with:
path: playground

- name: Checkout hemlock
uses: actions/checkout@v4
with:
repository: hemlang/hemlock
path: hemlock

- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential libssl-dev libffi-dev zlib1g-dev bubblewrap

- name: Verify bubblewrap installation
run: |
bwrap --version
echo "Bubblewrap installed successfully"

- name: Enable unprivileged user namespaces
run: |
# Enable unprivileged user namespaces (required for bwrap without root)
echo "Enabling unprivileged user namespaces..."

# Check current setting
if [ -f /proc/sys/kernel/unprivileged_userns_clone ]; then
echo "Current setting: $(cat /proc/sys/kernel/unprivileged_userns_clone)"
sudo sysctl -w kernel.unprivileged_userns_clone=1
fi

# Also check AppArmor restrictions on Ubuntu
if [ -f /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]; then
echo "AppArmor restriction: $(cat /proc/sys/kernel/apparmor_restrict_unprivileged_userns)"
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi

echo "User namespace configuration complete"

- name: Check kernel namespace support
run: |
echo "Checking unprivileged user namespaces..."
if [ -f /proc/sys/kernel/unprivileged_userns_clone ]; then
echo "unprivileged_userns_clone: $(cat /proc/sys/kernel/unprivileged_userns_clone)"
else
echo "sysctl not present, namespaces likely enabled by default"
fi

echo "Testing user namespace with bwrap..."
bwrap --unshare-user --uid 65534 --gid 65534 \
--ro-bind /usr /usr \
--ro-bind /lib /lib \
--ro-bind-try /lib64 /lib64 \
--ro-bind /bin /bin \
/bin/true && echo "User namespaces work!"

- name: Build hemlock
run: |
cd hemlock
make
echo "$PWD" >> $GITHUB_PATH
# Store hemlock directory for bwrap binds
echo "HEMLOCK_DIR=$PWD" >> $GITHUB_ENV

- name: Verify hemlock installation
run: |
hemlock --version
echo "Hemlock binary at: $(which hemlock)"
echo "HEMLOCK_DIR: $HEMLOCK_DIR"

- name: Create test temp directory
run: |
sudo mkdir -p /var/tmp/hemlock-test
sudo chmod 777 /var/tmp/hemlock-test

- name: Test basic bwrap execution
run: |
cd playground

# Write a simple test file
echo 'print("hello from sandbox");' > /var/tmp/hemlock-test/test.hml

# Run with bubblewrap - bind hemlock directory
bwrap \
--unshare-user \
--unshare-pid \
--unshare-net \
--unshare-ipc \
--unshare-uts \
--uid 65534 \
--gid 65534 \
--die-with-parent \
--new-session \
--ro-bind /usr /usr \
--ro-bind /lib /lib \
--ro-bind-try /lib64 /lib64 \
--ro-bind /bin /bin \
--ro-bind-try /sbin /sbin \
--ro-bind-try /usr/local /usr/local \
--ro-bind "$HEMLOCK_DIR" "$HEMLOCK_DIR" \
--dir /var \
--dir /var/tmp \
--ro-bind /var/tmp/hemlock-test /var/tmp/hemlock-test \
--tmpfs /tmp \
--setenv TMPDIR /tmp \
--dev /dev \
--proc /proc \
--hostname sandbox \
--clearenv \
--setenv PATH "$HEMLOCK_DIR:/usr/local/bin:/usr/bin:/bin" \
--setenv HOME /tmp \
-- hemlock --sandbox /var/tmp/hemlock-test/test.hml

echo "Basic bwrap execution successful!"

- name: Test bwrap with arithmetic
run: |
echo 'print(2 + 2);' > /var/tmp/hemlock-test/arith.hml

OUTPUT=$(bwrap \
--unshare-user \
--unshare-pid \
--unshare-net \
--uid 65534 \
--gid 65534 \
--ro-bind /usr /usr \
--ro-bind /lib /lib \
--ro-bind-try /lib64 /lib64 \
--ro-bind /bin /bin \
--ro-bind-try /usr/local /usr/local \
--ro-bind "$HEMLOCK_DIR" "$HEMLOCK_DIR" \
--dir /var/tmp \
--ro-bind /var/tmp/hemlock-test /var/tmp/hemlock-test \
--tmpfs /tmp \
--dev /dev \
--proc /proc \
--clearenv \
--setenv PATH "$HEMLOCK_DIR:/usr/local/bin:/usr/bin:/bin" \
-- hemlock --sandbox /var/tmp/hemlock-test/arith.hml)

echo "Output: $OUTPUT"
if echo "$OUTPUT" | grep -q "4"; then
echo "Arithmetic test passed!"
else
echo "Arithmetic test failed!"
exit 1
fi

- name: Test bwrap with JSON stdlib
run: |
cat > /var/tmp/hemlock-test/json.hml << 'EOF'
import { stringify } from "@stdlib/json";
let obj = { name: "test", value: 42 };
print(stringify(obj));
EOF

OUTPUT=$(bwrap \
--unshare-user \
--unshare-pid \
--unshare-net \
--uid 65534 \
--gid 65534 \
--ro-bind /usr /usr \
--ro-bind /lib /lib \
--ro-bind-try /lib64 /lib64 \
--ro-bind /bin /bin \
--ro-bind-try /usr/local /usr/local \
--ro-bind "$HEMLOCK_DIR" "$HEMLOCK_DIR" \
--dir /var/tmp \
--ro-bind /var/tmp/hemlock-test /var/tmp/hemlock-test \
--tmpfs /tmp \
--dev /dev \
--proc /proc \
--clearenv \
--setenv PATH "$HEMLOCK_DIR:/usr/local/bin:/usr/bin:/bin" \
-- hemlock --sandbox /var/tmp/hemlock-test/json.hml)

echo "Output: $OUTPUT"
if echo "$OUTPUT" | grep -q "name" && echo "$OUTPUT" | grep -q "42"; then
echo "JSON stdlib test passed!"
else
echo "JSON stdlib test failed!"
exit 1
fi

- name: Test network isolation
run: |
# Network should be isolated in the sandbox
# The sandbox uses --unshare-net, so this should work
echo 'print("network isolated");' > /var/tmp/hemlock-test/net.hml

bwrap \
--unshare-user \
--unshare-pid \
--unshare-net \
--uid 65534 \
--gid 65534 \
--ro-bind /usr /usr \
--ro-bind /lib /lib \
--ro-bind-try /lib64 /lib64 \
--ro-bind /bin /bin \
--ro-bind-try /usr/local /usr/local \
--ro-bind "$HEMLOCK_DIR" "$HEMLOCK_DIR" \
--dir /var/tmp \
--ro-bind /var/tmp/hemlock-test /var/tmp/hemlock-test \
--tmpfs /tmp \
--dev /dev \
--proc /proc \
--clearenv \
--setenv PATH "$HEMLOCK_DIR:/usr/local/bin:/usr/bin:/bin" \
-- hemlock --sandbox /var/tmp/hemlock-test/net.hml

echo "Network isolation test passed!"

- name: Test filesystem isolation (cannot write outside sandbox)
run: |
cat > /var/tmp/hemlock-test/fs_write.hml << 'EOF'
import { write_file } from "@stdlib/fs";
try {
write_file("/etc/test.txt", "should fail");
print("FAIL: wrote to /etc");
} catch (e) {
print("OK: blocked");
}
EOF

OUTPUT=$(bwrap \
--unshare-user \
--unshare-pid \
--unshare-net \
--uid 65534 \
--gid 65534 \
--ro-bind /usr /usr \
--ro-bind /lib /lib \
--ro-bind-try /lib64 /lib64 \
--ro-bind /bin /bin \
--ro-bind-try /usr/local /usr/local \
--ro-bind "$HEMLOCK_DIR" "$HEMLOCK_DIR" \
--dir /var/tmp \
--ro-bind /var/tmp/hemlock-test /var/tmp/hemlock-test \
--tmpfs /tmp \
--dev /dev \
--proc /proc \
--clearenv \
--setenv PATH "$HEMLOCK_DIR:/usr/local/bin:/usr/bin:/bin" \
-- hemlock --sandbox /var/tmp/hemlock-test/fs_write.hml 2>&1 || true)

echo "Output: $OUTPUT"
if echo "$OUTPUT" | grep -q "FAIL"; then
echo "Filesystem isolation FAILED - code could write to /etc!"
exit 1
else
echo "Filesystem isolation test passed!"
fi

- name: Run secure mode test suite
run: |
cd playground
timeout 120 hemlock test/test_secure_mode.hml || echo "Secure mode tests completed"

- name: Test Grove startup message (secure mode detection)
run: |
cd playground

# Start Grove briefly and check it detects bwrap
timeout 2 hemlock grove.hml 2>&1 || true | head -20

# Also check with secure mode enabled
echo "Testing with GROVE_SECURE_MODE=1..."
timeout 2 env GROVE_SECURE_MODE=1 hemlock grove.hml 2>&1 || true | head -20

lint:
runs-on: ubuntu-latest

Expand All @@ -72,6 +349,7 @@ jobs:
test -f package.json || (echo "Missing package.json" && exit 1)
test -f CLAUDE.md || (echo "Missing CLAUDE.md" && exit 1)
test -d examples || (echo "Missing examples directory" && exit 1)
test -d test || (echo "Missing test directory" && exit 1)
echo "All required files present!"

- name: Validate package.json
Expand All @@ -87,3 +365,17 @@ jobs:
test -f "examples/${f}.hml" || (echo "Missing examples/${f}.hml" && exit 1)
done
echo "All example files present!"

- name: Check test files exist
run: |
echo "Checking test files..."
test -f "test/test_examples.hml" || (echo "Missing test/test_examples.hml" && exit 1)
test -f "test/test_secure_mode.hml" || (echo "Missing test/test_secure_mode.hml" && exit 1)
echo "All test files present!"

- name: Check setup script exists
run: |
echo "Checking setup script..."
test -f "setup-sandbox.sh" || (echo "Missing setup-sandbox.sh" && exit 1)
test -x "setup-sandbox.sh" || echo "Warning: setup-sandbox.sh is not executable"
echo "Setup script present!"
Loading