Skip to content

Commit 50de572

Browse files
authored
Merge pull request #48 from link-foundation/issue-46-479c0ca36ec0
Fix Permission denied for sandbox user JSON access (Issue #46)
2 parents d1004ea + 2779dcb commit 50de572

File tree

3 files changed

+89
-27
lines changed

3 files changed

+89
-27
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
bump: patch
3+
---
4+
5+
Fix CI failure: Permission denied when sandbox user reads JSON file (Issue #46)
6+
7+
The realpath fix from v1.3.5 resolved the "No such file or directory" error but not the
8+
"Permission denied" error. The GitHub Actions workspace root (/home/runner/work/sandbox/sandbox/)
9+
has mode 750 (owned by runner), blocking the sandbox user from traversing into it.
10+
11+
Fix: copy the JSON measurements file to /tmp (world-accessible, mode 1777) before running
12+
the sandbox user subprocess, then copy the result back. This avoids any need for the sandbox
13+
user to traverse runner-owned directories.

docs/case-studies/issue-46/CASE-STUDY.md

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,34 @@
1-
# Case Study: Failing CI/CD Run — "No such file or directory" for JSON Measurements
1+
# Case Study: Failing CI/CD Run — Permission Denied for JSON Measurements
22

33
**Issue**: [#46 - Fix failing CI/CD run](https://github.com/link-foundation/sandbox/issues/46)
4-
**CI Run**: [22261112919, Job 64399507098](https://github.com/link-foundation/sandbox/actions/runs/22261112919/job/64399507098)
4+
**CI Runs**:
5+
- [22261112919, Job 64399507098](https://github.com/link-foundation/sandbox/actions/runs/22261112919/job/64399507098) — First failure: "No such file or directory"
6+
- [22263724056](https://github.com/link-foundation/sandbox/actions/runs/22263724056/job/64405913545) — Second failure: "Permission denied"
7+
58
**Date**: 2026-02-21
6-
**Status**: Investigation Complete — Fix Applied
9+
**Status**: Investigation Complete — Fix Applied (v1.3.6)
710

811
## Executive Summary
912

10-
The "Measure Disk Space and Update README" workflow failed with:
13+
This issue has two related but distinct failures:
14+
15+
### First Failure (Run 22261112919) — "No such file or directory"
1116

1217
```
1318
cat: data/disk-space-measurements.json: No such file or directory
1419
##[error]Process completed with exit code 1.
1520
```
1621

17-
The root cause is that `su - sandbox` (login shell) changes the working directory to the sandbox user's home (`/home/sandbox`). The relative path `data/disk-space-measurements.json` then resolves to `/home/sandbox/data/disk-space-measurements.json` — a file that was never created there. The JSON file was correctly initialized in the runner's working directory (`/home/runner/work/sandbox/sandbox/data/disk-space-measurements.json`), but the sandbox-measure.sh script tried to read and write a different file.
22+
Root cause: `su - sandbox` (login shell) changes CWD to `/home/sandbox`. The relative path `data/disk-space-measurements.json` then resolves to `/home/sandbox/data/disk-space-measurements.json` — a path that was never created. **Fix**: Convert the relative JSON path to an absolute path using `realpath` before passing to `su - sandbox`. Applied in PR #47 (v1.3.5).
1823

19-
**Fix**: Convert the relative JSON output path to an absolute path before passing it to `su - sandbox`.
24+
### Second Failure (Run 22263724056) — "Permission denied"
25+
26+
```
27+
cat: /home/runner/work/sandbox/sandbox/data/disk-space-measurements.json: Permission denied
28+
##[error]Process completed with exit code 1.
29+
```
30+
31+
Root cause: After applying the `realpath` fix, the absolute path was passed correctly, but the sandbox user still could not access the file. The GitHub Actions workspace directory `/home/runner/work/sandbox/sandbox/` is owned by `runner` and has permissions `750`, denying traverse access to the `sandbox` user (who is not in the `runner` group). The first fix granted `o+rx` only to the immediate parent `data/` directory, but not to the workspace root — so the kernel path traversal check still failed at `/home/runner/work/sandbox/sandbox/`. **Fix**: Copy the JSON file to `/tmp/` (world-accessible, mode `1777`) before running the sandbox user script, then copy it back. Applied in v1.3.6.
2032

2133
## Timeline of Events
2234

@@ -97,13 +109,14 @@ This directory (`/home/sandbox/data/`) was never created, making the path comple
97109

98110
## Solution
99111

100-
### Fix: Convert to Absolute Path Before `su - sandbox`
112+
### Fix 1 (v1.3.5, PR #47): Convert to Absolute Path Before `su - sandbox`
101113

102-
The fix is to resolve the JSON output path to an absolute path before executing the sandbox user's sub-script. This ensures the path remains valid regardless of what working directory the sub-shell starts in.
114+
Resolve the JSON output path to an absolute path before executing the sandbox user's sub-script. This ensures the path remains valid regardless of what working directory the sub-shell starts in.
103115

104116
```bash
105-
# Convert JSON_OUTPUT_FILE to absolute path before passing to su
106117
JSON_OUTPUT_FILE_ABS="$(realpath "$JSON_OUTPUT_FILE")"
118+
chmod o+rw "$JSON_OUTPUT_FILE_ABS"
119+
chmod o+rx "$(dirname "$JSON_OUTPUT_FILE_ABS")"
107120

108121
if [ "$EUID" -eq 0 ]; then
109122
su - sandbox -c "bash /tmp/sandbox-measure.sh '$JSON_OUTPUT_FILE_ABS'"
@@ -112,28 +125,54 @@ else
112125
fi
113126
```
114127

115-
Additionally, the `sandbox-measure.sh` script needs to create the parent directory if it doesn't exist, since the sandbox user may not have the `data/` directory in their home:
128+
This resolved the "No such file or directory" error but not the "Permission denied" error.
129+
130+
### Fix 2 (v1.3.6, PR #48): Copy JSON File to `/tmp/` for Sandbox User Access
131+
132+
The deeper problem is that the sandbox user cannot traverse the workspace directory tree. The GitHub Actions workspace root (`/home/runner/work/sandbox/sandbox/`) is owned by `runner` with permissions `750`. The `sandbox` user is not in the `runner` group, so they get `EACCES` when trying to traverse the directory — even if the file itself is world-readable.
133+
134+
The fix copies the JSON file to `/tmp/` (world-accessible, mode `1777`), runs the sandbox user script against that copy, then copies it back:
116135

117136
```bash
118-
# At the start of sandbox-measure.sh
119-
mkdir -p "$(dirname "$JSON_OUTPUT_FILE")"
137+
JSON_OUTPUT_FILE_ABS="$(realpath "$JSON_OUTPUT_FILE")"
138+
JSON_TMP_COPY="$(mktemp /tmp/disk-space-measurements-XXXXXX.json)"
139+
cp "$JSON_OUTPUT_FILE_ABS" "$JSON_TMP_COPY"
140+
chmod o+rw "$JSON_TMP_COPY"
141+
142+
if [ "$EUID" -eq 0 ]; then
143+
su - sandbox -c "bash /tmp/sandbox-measure.sh '$JSON_TMP_COPY'"
144+
else
145+
sudo -i -u sandbox bash /tmp/sandbox-measure.sh "$JSON_TMP_COPY"
146+
fi
147+
148+
cp "$JSON_TMP_COPY" "$JSON_OUTPUT_FILE_ABS"
149+
rm -f "$JSON_TMP_COPY"
120150
```
121151

122-
### Why This Fix Is Correct
152+
### Why Fix 2 Is Correct
153+
154+
For the sandbox user to access a file at `/home/runner/work/sandbox/sandbox/data/measurements.json`, they need execute (`x`) permission on **every directory** in the path:
155+
156+
| Directory | Owner | Typical mode | Sandbox user can traverse? |
157+
|-----------|-------|-------------|---------------------------|
158+
| `/home/runner/` | runner | 755 | Yes (world x) |
159+
| `/home/runner/work/` | runner | 755 | Yes (world x) |
160+
| `/home/runner/work/sandbox/` | runner | 755 | Yes (world x) |
161+
| `/home/runner/work/sandbox/sandbox/` | runner | **750** | **No** (no world x) |
162+
| `.../data/` | root | was set to o+rx by Fix 1 | Yes |
163+
| `.../measurements.json` | root | was set to o+rw by Fix 1 | Yes |
123164

124-
- `realpath` converts relative paths to absolute paths using the current working directory
125-
- The absolute path `/home/runner/work/sandbox/sandbox/data/disk-space-measurements.json` is unambiguous and valid from any working directory
126-
- The sandbox user needs write access to this path — since the runner runs with elevated privileges and the outer script (running as root) creates the file, the sandbox user needs read/write access, which can be ensured by pre-creating the file with appropriate permissions
165+
Fix 1 set `o+rx` on `data/` and `o+rw` on the file but missed `/home/runner/work/sandbox/sandbox/` which has no world-execute bit. Fix 2 sidesteps the issue entirely by using `/tmp/` which is world-accessible (`1777`).
127166

128167
### Alternative Approaches Considered
129168

130-
1. **Use `su` without `-`** (no login shell): Would preserve the working directory but break user-space tool installations that expect a clean home environment. Not recommended.
169+
1. **Walk all ancestor directories and chmod**: `chmod o+rx` every directory from `/` to `data/`. This works but widens permissions on runner-owned directories unnecessarily and has a larger blast radius.
131170

132-
2. **Pass absolute path directly from CLI**: Requires users to always specify absolute paths in the workflow, which is error-prone.
171+
2. **Use `su` without `-`** (no login shell): Would preserve the working directory but break user-space tool installations that expect a clean home environment. Not recommended.
133172

134-
3. **Use `cd` inside `su -` command**: Adding `cd /path/to/workdir &&` before the script call would work but is fragile.
173+
3. **Pass absolute path directly from CLI**: Requires users to always specify absolute paths in the workflow, which is error-prone.
135174

136-
4. **Write JSON to `/tmp/` instead of `data/`**: Would avoid the working directory issue but changes the documented output location.
175+
4. **Use `cd` inside `su -` command**: Adding `cd /path/to/workdir &&` before the script call would work but is fragile.
137176

138177
## Technical Details
139178

scripts/measure-disk-space.sh

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -648,17 +648,27 @@ chmod +x /tmp/sandbox-measure.sh
648648
# location, causing "No such file or directory" errors.
649649
# See docs/case-studies/issue-46 for the full root cause analysis.
650650
JSON_OUTPUT_FILE_ABS="$(realpath "$JSON_OUTPUT_FILE")"
651-
# Ensure sandbox user can read/write the JSON file (root created it)
652-
chmod o+rw "$JSON_OUTPUT_FILE_ABS"
653-
chmod o+rx "$(dirname "$JSON_OUTPUT_FILE_ABS")"
654-
655-
# Execute sandbox user measurements
651+
# The sandbox user needs to read and write the JSON file, but the GitHub Actions
652+
# workspace directories (e.g. /home/runner/work/sandbox/sandbox/) are owned by
653+
# 'runner' and typically have mode 750, so 'sandbox' (not in the 'runner' group)
654+
# cannot traverse them even if the file itself is world-readable.
655+
# Solution: copy the JSON file to /tmp (world-accessible, mode 1777), run the
656+
# sandbox measurements against that copy, then copy the result back.
657+
JSON_TMP_COPY="$(mktemp /tmp/disk-space-measurements-XXXXXX.json)"
658+
cp "$JSON_OUTPUT_FILE_ABS" "$JSON_TMP_COPY"
659+
chmod o+rw "$JSON_TMP_COPY"
660+
661+
# Execute sandbox user measurements against the /tmp copy
656662
if [ "$EUID" -eq 0 ]; then
657-
su - sandbox -c "bash /tmp/sandbox-measure.sh '$JSON_OUTPUT_FILE_ABS'"
663+
su - sandbox -c "bash /tmp/sandbox-measure.sh '$JSON_TMP_COPY'"
658664
else
659-
sudo -i -u sandbox bash /tmp/sandbox-measure.sh "$JSON_OUTPUT_FILE_ABS"
665+
sudo -i -u sandbox bash /tmp/sandbox-measure.sh "$JSON_TMP_COPY"
660666
fi
661667

668+
# Copy the updated measurements back to the original location
669+
cp "$JSON_TMP_COPY" "$JSON_OUTPUT_FILE_ABS"
670+
rm -f "$JSON_TMP_COPY"
671+
662672
rm -f /tmp/sandbox-measure.sh
663673

664674
# ============================================================================

0 commit comments

Comments
 (0)