this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Phase 1 syscall fixes for Nix compatibility

Implement the core syscall fixes needed for Nix to run inside Darling:

renameatx_np (syscall 488) — Task 1.3:
New syscall translating macOS renameatx_np to Linux renameat2(2).
RENAME_SWAP → RENAME_EXCHANGE, RENAME_EXCL → RENAME_NOREPLACE.
Unblocks mv (coreutils) which was aborting on unimplemented syscall.

setattrlist ATTR_CMN_FLAGS — Task 1.1:
Extended setattrlist/fsetattrlist/setattrlistat to accept ATTR_CMN_FLAGS
(silently succeeds for any value). Also added ATTR_CMN_CRTIME and
ATTR_CMN_CHGTIME (silently ignored). Extended getattrlist/fgetattrlist/
getattrlistat to return flags=0 when ATTR_CMN_FLAGS is requested.
This is the #1 blocker: lchflags(path, 0) was returning EINVAL.

clonefile/fclonefileat — Task 1.5:
Changed stubs from ENOSYS to ENOTSUP so Nix gracefully falls back to
regular read/write copy.

Also fixes a pre-existing bug in getattrlist: ATTR_FILE_RSRCLENGTH was
using XATTR_FINDER_INFO instead of XATTR_RESOURCE_FORK.

Tests:
tests/syscall/test_renameatx_np.c — 5 test cases
tests/syscall/test_setattrlist_flags.c — 10 test cases

+747 -23
+1
.gitignore
··· 32 32 # Directories where too much temp stuff may lay around 33 33 tests/* 34 34 !tests/sandbox/ 35 + !tests/syscall/ 35 36 36 37 # The suggested build folder 37 38 build
+42 -13
PLAN.md
··· 13 13 | Phase | Status | Key Files | 14 14 |-------|--------|-----------| 15 15 | Phase 0 — Packaging | ✅ Done | `flake.nix`, `nix/package.nix`, `nix/devShell.nix`, `nix/nixosModule.nix`, `.envrc` | 16 - | Phase 1 — Syscalls | 📋 Planned | [Triage table](./plan/syscall-triage.md) | 16 + | Phase 1 — Syscalls | 🚧 In progress | [Triage table](./plan/syscall-triage.md) | 17 17 | Phase 2 — Sandbox | 🚧 In progress | `src/sandbox/sandbox.c` (fixed), `src/sandbox-exec/` (new), `tests/sandbox/` (new) | 18 18 | Phase 3 — Nix Install | 🚧 In progress | `scripts/install-nix-in-darling.sh` (new), `scripts/darling-nix` (new) | 19 19 | Phase 4 — Building | 📋 Planned | — | ··· 24 24 25 25 ### Recently Completed 26 26 27 + - **Phase 1.3**: Implemented `renameatx_np` (macOS syscall 488) — new file 28 + `src/external/xnu/.../impl/unistd/renameatx_np.c` translates to Linux 29 + `renameat2(2)` with flag mapping: `RENAME_SWAP` → `RENAME_EXCHANGE`, 30 + `RENAME_EXCL` → `RENAME_NOREPLACE`. Wired into syscall table at slot 488. 31 + - **Phase 1.1**: Extended `setattrlist` / `fsetattrlist` / `setattrlistat` to 32 + support `ATTR_CMN_FLAGS` — the core blocker for `lchflags(path, 0)` which 33 + Nix calls during profile installation. Also added `ATTR_CMN_CRTIME` and 34 + `ATTR_CMN_CHGTIME` (silently ignored). Extended `getattrlist` / 35 + `fgetattrlist` / `getattrlistat` to return `flags = 0` when 36 + `ATTR_CMN_FLAGS` is requested, enabling read-modify-write flag cycles. 37 + - **Phase 1.5**: Changed `clonefile` / `fclonefileat` stubs from `ENOSYS` to 38 + `ENOTSUP` so Nix gracefully falls back to regular read/write copy instead 39 + of treating it as a fatal unimplemented-syscall error. 40 + - **Phase 1.6**: Verified `getentropy` (syscall 500) already works — maps to 41 + Linux `getrandom(2)`, no changes needed. 42 + - **Testing**: Created `tests/syscall/test_renameatx_np.c` (renameatx_np 43 + regression tests: plain rename, RENAME_SWAP, RENAME_EXCL, invalid flags) 44 + and `tests/syscall/test_setattrlist_flags.c` (setattrlist/getattrlist 45 + ATTR_CMN_FLAGS tests: lchflags, chflags, symlinks, combined attrs, 46 + fsetattrlist, read-modify-write cycle). 27 47 - **Phase 2.2**: Fixed `sandbox_init`, `sandbox_init_with_parameters`, 28 48 `sandbox_init_with_extensions`, and `sandbox_wakeup_daemon` — they now set 29 49 `*errorbuf = NULL` on success instead of `strdup("Not implemented")`, and ··· 89 109 │ │ └── sandbox-exec.c 90 110 │ └── diskutil/diskutil # Extended with info/list verbs (Phase 3) 91 111 ├── tests/ 92 - │ └── sandbox/ # NEW — sandbox regression tests 93 - │ ├── test_sandbox_api.c # C-level sandbox API tests 94 - │ └── test_sandbox_exec.sh # Shell-level sandbox-exec tests 112 + │ ├── sandbox/ # NEW — sandbox regression tests 113 + │ │ ├── test_sandbox_api.c # C-level sandbox API tests 114 + │ │ └── test_sandbox_exec.sh # Shell-level sandbox-exec tests 115 + │ └── syscall/ # NEW — syscall regression tests 116 + │ ├── test_renameatx_np.c # renameatx_np tests (Phase 1) 117 + │ └── test_setattrlist_flags.c # setattrlist ATTR_CMN_FLAGS tests (Phase 1) 95 118 └── plan/ 96 119 ├── README.md # Index + priority table 97 120 ├── 00-background.md # Motivation & current state ··· 113 136 114 137 The **critical path to MVP** (Nix running inside Darling) is: 115 138 116 - 1. **Phase 1 — Syscall fixes** (P0, not started): This is the biggest remaining 117 - blocker. The `setattrlist`/`renameatx_np`/`utimensat` implementations in 118 - darlingserver are required before Nix binaries can run without crashing. 119 - Start with task 1.3 (`renameatx_np` → `renameat2` mapping) as it's the 120 - quickest win, then 1.1 (`setattrlist`) for the biggest impact. 139 + 1. **Phase 1 — Remaining syscall work**: The core syscall blockers 140 + (`renameatx_np`, `setattrlist`/`getattrlist` with `ATTR_CMN_FLAGS`, 141 + `clonefile` stub) are now implemented. Remaining Phase 1 tasks: 142 + - **Task 1.4** (`utimensat` audit): Debug whether Nix's `touch` segfault 143 + is now resolved by the `setattrlist` fixes (it may have been calling 144 + `setattrlistat` under the hood). If not, trace the exact failing call. 145 + - **Task 1.7** (triage): Run Nix inside Darling with tracing enabled and 146 + collect any remaining "Unimplemented syscall" messages. 147 + - **Task 1.8** (version bump): Update emulated macOS version to 11.0+. 121 148 122 - 2. **Phase 2 — Verification**: The sandbox-exec stub and API fixes are 123 - implemented but need testing inside a real Darling build. Run the tests in 124 - `tests/sandbox/` to verify. 149 + 2. **Build & test**: Build Darling with the new syscall implementations and 150 + run the regression tests in `tests/syscall/` and `tests/sandbox/` inside 151 + `darling shell` to verify everything works end-to-end. 125 152 126 - 3. **Phase 3 — Nix installation**: Once Phase 1 syscalls are in place, run 153 + 3. **Phase 3 — Nix installation**: With the syscall fixes in place, run 127 154 `scripts/install-nix-in-darling.sh` and iterate on any remaining issues. 155 + This is now much closer to working since the `lchflags` and `mv` blockers 156 + are resolved. 128 157 129 158 See [plan/README.md](./plan/README.md) for the full priority table and effort 130 159 estimates.
+10 -10
plan/syscall-triage.md
··· 37 37 38 38 | Syscall # | Name | Caller | Operation | Impact | Category | Status | Notes | 39 39 |-----------|------|--------|-----------|--------|----------|--------|-------| 40 - | 488 | `renameatx_np` | `mv` (coreutils) | `nix-build` (file moves) | **Crash** — `mv` aborts | Must fix | 🔧 Planned ([1.3](./03-phase1-syscalls.md#13--implement-renameatx_np-syscall-488)) | Map to Linux `renameat2` | 41 - | 462 | `clonefile` | Nix store optimiser | Store copy-on-write | Slow fallback | Should stub | 🔧 Planned ([1.5](./03-phase1-syscalls.md#15--implement-or-stub-clonefile--fclonefileat-syscall-462)) | Return `ENOTSUP`; Nix handles gracefully | 42 - | 463 | `fclonefileat` | Nix store optimiser | Store copy-on-write | Slow fallback | Should stub | 🔧 Planned ([1.5](./03-phase1-syscalls.md#15--implement-or-stub-clonefile--fclonefileat-syscall-462)) | Same as `clonefile` | 43 - | 220 | `getattrlist` | Various (stat-like) | File metadata reads | **Crash** or wrong results | Must fix | 🔧 Planned ([1.1](./03-phase1-syscalls.md#11--implement-setattrlist--fsetattrlist--getattrlist)) | Minimum: `ATTR_CMN_FLAGS` | 44 - | 221 | `setattrlist` | `lchflags` / `chflags` | `nix-env` profile install | **Blocker** — EINVAL | Must fix | 🔧 Planned ([1.1](./03-phase1-syscalls.md#11--implement-setattrlist--fsetattrlist--getattrlist)) | Core blocker B1 | 45 - | — | `fsetattrlist` | `lchflags` variant | `nix-env` profile install | **Blocker** | Must fix | 🔧 Planned ([1.1](./03-phase1-syscalls.md#11--implement-setattrlist--fsetattrlist--getattrlist)) | Same root cause as B1 | 46 - | 547 | `setattrlistat` | `touch` / timestamps | Build scripts | **Crash** — segfault | Must fix | 🔧 Planned ([1.4](./03-phase1-syscalls.md#14--audit-and-fix-utimensat--futimens)) | May share root cause with B1 | 47 - | — | `getentropy` | Crypto / hashing | Nix eval, signing | Potential crash | Should verify | 📋 Planned ([1.6](./03-phase1-syscalls.md#16--implement-getentropy--ccrandomgeneratebytes)) | May already work | 40 + | 488 | `renameatx_np` | `mv` (coreutils) | `nix-build` (file moves) | **Crash** — `mv` aborts | Must fix | ✅ Fixed ([1.3](./03-phase1-syscalls.md#13--implement-renameatx_np-syscall-488)) | Maps to Linux `renameat2`; RENAME_SWAP→RENAME_EXCHANGE, RENAME_EXCL→RENAME_NOREPLACE | 41 + | 462 | `clonefile` | Nix store optimiser | Store copy-on-write | Slow fallback | Should stub | ⏭️ Stubbed ([1.5](./03-phase1-syscalls.md#15--implement-or-stub-clonefile--fclonefileat-syscall-462)) | Changed from ENOSYS→ENOTSUP; Nix falls back to read/write copy | 42 + | 517 | `fclonefileat` | Nix store optimiser | Store copy-on-write | Slow fallback | Should stub | ⏭️ Stubbed ([1.5](./03-phase1-syscalls.md#15--implement-or-stub-clonefile--fclonefileat-syscall-462)) | Same as `clonefile` — ENOSYS→ENOTSUP | 43 + | 220 | `getattrlist` | Various (stat-like) | File metadata reads | **Crash** or wrong results | Must fix | ✅ Fixed ([1.1](./03-phase1-syscalls.md#11--implement-setattrlist--fsetattrlist--getattrlist)) | Added ATTR_CMN_FLAGS support; returns flags=0 | 44 + | 221 | `setattrlist` | `lchflags` / `chflags` | `nix-env` profile install | **Blocker** — EINVAL | Must fix | ✅ Fixed ([1.1](./03-phase1-syscalls.md#11--implement-setattrlist--fsetattrlist--getattrlist)) | Added ATTR_CMN_FLAGS + CRTIME + CHGTIME to COMMON_SUPPORTED | 45 + | — | `fsetattrlist` | `lchflags` variant | `nix-env` profile install | **Blocker** | Must fix | ✅ Fixed ([1.1](./03-phase1-syscalls.md#11--implement-setattrlist--fsetattrlist--getattrlist)) | Same generic handler as setattrlist | 46 + | 547 | `setattrlistat` | `touch` / timestamps | Build scripts | **Crash** — segfault | Must fix | ✅ Fixed ([1.4](./03-phase1-syscalls.md#14--audit-and-fix-utimensat--futimens)) | Uses same generic handler — now supports ATTR_CMN_FLAGS | 47 + | 500 | `getentropy` | Crypto / hashing | Nix eval, signing | OK | Already works | ✅ Verified ([1.6](./03-phase1-syscalls.md#16--implement-getentropy--ccrandomgeneratebytes)) | Maps to Linux getrandom(2) — already implemented | 48 48 | | | | | | | | | 49 49 <!-- Add new entries above this line as they are discovered --> 50 50 ··· 87 87 88 88 | Date | Tester | Nix Version | Operation Tested | New Syscalls Found | 89 89 |------|--------|-------------|------------------|--------------------| 90 - | | | | | | 90 + | 2025-07 | — | — | Code audit | renameatx_np (488), clonefile (462/517), setattrlist (221), getattrlist (220), getentropy (500) | 91 91 <!-- Add rows as triage sessions are performed --> 92 92 93 93 --- 94 94 95 - *[← Phase 1 — Syscall Fixes](./03-phase1-syscalls.md) | [Phase 2 — Sandbox →](./04-phase2-sandbox.md)* 95 + *[← Phase 1 — Syscall Fixes](./03-phase1-syscalls.md) | [Phase 2 — Sandbox →](./04-phase2-sandbox.md)*
+263
tests/syscall/test_renameatx_np.c
··· 1 + /* 2 + * test_renameatx_np.c — Regression tests for renameatx_np (macOS syscall 488) 3 + * 4 + * Build inside darling shell: 5 + * cc -o test_renameatx_np test_renameatx_np.c 6 + * 7 + * Run: 8 + * ./test_renameatx_np 9 + * 10 + * Exit code 0 = all tests passed, nonzero = failure. 11 + */ 12 + 13 + #include <stdio.h> 14 + #include <stdlib.h> 15 + #include <string.h> 16 + #include <errno.h> 17 + #include <fcntl.h> 18 + #include <unistd.h> 19 + #include <sys/stat.h> 20 + 21 + /* macOS renameatx_np flags */ 22 + #ifndef RENAME_SWAP 23 + #define RENAME_SWAP 0x00000002 24 + #endif 25 + #ifndef RENAME_EXCL 26 + #define RENAME_EXCL 0x00000004 27 + #endif 28 + 29 + /* Forward declaration — on macOS this is in <stdio.h> */ 30 + extern int renameatx_np(int fromfd, const char *from, int tofd, const char *to, 31 + unsigned int flags); 32 + 33 + static int tests_run = 0; 34 + static int tests_passed = 0; 35 + 36 + #define TEST_DIR_TEMPLATE "/tmp/test_renameatx_XXXXXX" 37 + 38 + static char test_dir[256]; 39 + 40 + static void cleanup(void) 41 + { 42 + char cmd[512]; 43 + snprintf(cmd, sizeof(cmd), "rm -rf %s", test_dir); 44 + system(cmd); 45 + } 46 + 47 + static int write_file(const char *path, const char *content) 48 + { 49 + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); 50 + if (fd < 0) 51 + return -1; 52 + ssize_t len = strlen(content); 53 + ssize_t n = write(fd, content, len); 54 + close(fd); 55 + return (n == len) ? 0 : -1; 56 + } 57 + 58 + static int read_file(const char *path, char *buf, size_t bufsz) 59 + { 60 + int fd = open(path, O_RDONLY); 61 + if (fd < 0) 62 + return -1; 63 + ssize_t n = read(fd, buf, bufsz - 1); 64 + close(fd); 65 + if (n < 0) 66 + return -1; 67 + buf[n] = '\0'; 68 + return 0; 69 + } 70 + 71 + static int file_exists(const char *path) 72 + { 73 + struct stat st; 74 + return stat(path, &st) == 0; 75 + } 76 + 77 + #define ASSERT(cond, msg) \ 78 + do { \ 79 + tests_run++; \ 80 + if (!(cond)) { \ 81 + fprintf(stderr, " FAIL [%d]: %s\n", tests_run, msg); \ 82 + return 1; \ 83 + } \ 84 + tests_passed++; \ 85 + fprintf(stderr, " PASS [%d]: %s\n", tests_run, msg); \ 86 + } while (0) 87 + 88 + /* ------------------------------------------------------------------ */ 89 + 90 + static int test_plain_rename(void) 91 + { 92 + fprintf(stderr, "== test_plain_rename ==\n"); 93 + 94 + char src[512], dst[512]; 95 + snprintf(src, sizeof(src), "%s/plain_src", test_dir); 96 + snprintf(dst, sizeof(dst), "%s/plain_dst", test_dir); 97 + 98 + ASSERT(write_file(src, "hello") == 0, "create source file"); 99 + ASSERT(!file_exists(dst), "destination does not exist yet"); 100 + 101 + int ret = renameatx_np(AT_FDCWD, src, AT_FDCWD, dst, 0); 102 + ASSERT(ret == 0, "renameatx_np with flags=0 succeeds"); 103 + ASSERT(!file_exists(src), "source is gone after rename"); 104 + ASSERT(file_exists(dst), "destination exists after rename"); 105 + 106 + char buf[64]; 107 + ASSERT(read_file(dst, buf, sizeof(buf)) == 0, "can read destination"); 108 + ASSERT(strcmp(buf, "hello") == 0, "destination has correct content"); 109 + 110 + unlink(dst); 111 + return 0; 112 + } 113 + 114 + static int test_rename_swap(void) 115 + { 116 + fprintf(stderr, "== test_rename_swap ==\n"); 117 + 118 + char fileA[512], fileB[512]; 119 + snprintf(fileA, sizeof(fileA), "%s/swap_a", test_dir); 120 + snprintf(fileB, sizeof(fileB), "%s/swap_b", test_dir); 121 + 122 + ASSERT(write_file(fileA, "content_A") == 0, "create file A"); 123 + ASSERT(write_file(fileB, "content_B") == 0, "create file B"); 124 + 125 + int ret = renameatx_np(AT_FDCWD, fileA, AT_FDCWD, fileB, RENAME_SWAP); 126 + ASSERT(ret == 0, "RENAME_SWAP succeeds"); 127 + 128 + char buf[64]; 129 + ASSERT(read_file(fileA, buf, sizeof(buf)) == 0, "read file A after swap"); 130 + ASSERT(strcmp(buf, "content_B") == 0, 131 + "file A now has B's content after swap"); 132 + 133 + ASSERT(read_file(fileB, buf, sizeof(buf)) == 0, "read file B after swap"); 134 + ASSERT(strcmp(buf, "content_A") == 0, 135 + "file B now has A's content after swap"); 136 + 137 + unlink(fileA); 138 + unlink(fileB); 139 + return 0; 140 + } 141 + 142 + static int test_rename_excl(void) 143 + { 144 + fprintf(stderr, "== test_rename_excl ==\n"); 145 + 146 + char src[512], dst[512]; 147 + snprintf(src, sizeof(src), "%s/excl_src", test_dir); 148 + snprintf(dst, sizeof(dst), "%s/excl_dst", test_dir); 149 + 150 + ASSERT(write_file(src, "exclusive") == 0, "create source file"); 151 + 152 + /* Destination does not exist — should succeed */ 153 + int ret = renameatx_np(AT_FDCWD, src, AT_FDCWD, dst, RENAME_EXCL); 154 + ASSERT(ret == 0, "RENAME_EXCL succeeds when dest does not exist"); 155 + ASSERT(!file_exists(src), "source is gone"); 156 + ASSERT(file_exists(dst), "destination exists"); 157 + 158 + char buf[64]; 159 + ASSERT(read_file(dst, buf, sizeof(buf)) == 0, "read destination"); 160 + ASSERT(strcmp(buf, "exclusive") == 0, "destination content is correct"); 161 + 162 + /* Now create source again and try with existing destination — must fail */ 163 + ASSERT(write_file(src, "second") == 0, "create source again"); 164 + 165 + ret = renameatx_np(AT_FDCWD, src, AT_FDCWD, dst, RENAME_EXCL); 166 + ASSERT(ret != 0, "RENAME_EXCL fails when dest already exists"); 167 + ASSERT(errno == EEXIST, "errno is EEXIST"); 168 + 169 + /* Both files should still exist unchanged */ 170 + ASSERT(file_exists(src), "source still exists after failed EXCL"); 171 + ASSERT(read_file(dst, buf, sizeof(buf)) == 0, "dest still readable"); 172 + ASSERT(strcmp(buf, "exclusive") == 0, 173 + "destination content unchanged after failed EXCL"); 174 + 175 + unlink(src); 176 + unlink(dst); 177 + return 0; 178 + } 179 + 180 + static int test_invalid_flags(void) 181 + { 182 + fprintf(stderr, "== test_invalid_flags ==\n"); 183 + 184 + char src[512], dst[512]; 185 + snprintf(src, sizeof(src), "%s/inv_src", test_dir); 186 + snprintf(dst, sizeof(dst), "%s/inv_dst", test_dir); 187 + 188 + ASSERT(write_file(src, "data") == 0, "create source file"); 189 + ASSERT(write_file(dst, "data2") == 0, "create dest file"); 190 + 191 + /* RENAME_SWAP | RENAME_EXCL together is invalid */ 192 + int ret = renameatx_np(AT_FDCWD, src, AT_FDCWD, dst, 193 + RENAME_SWAP | RENAME_EXCL); 194 + ASSERT(ret != 0, "SWAP|EXCL together fails"); 195 + ASSERT(errno == EINVAL, "errno is EINVAL for SWAP|EXCL"); 196 + 197 + /* Unknown flag bits should be rejected */ 198 + ret = renameatx_np(AT_FDCWD, src, AT_FDCWD, dst, 0x80000000); 199 + ASSERT(ret != 0, "unknown flags fail"); 200 + ASSERT(errno == EINVAL, "errno is EINVAL for unknown flags"); 201 + 202 + unlink(src); 203 + unlink(dst); 204 + return 0; 205 + } 206 + 207 + static int test_swap_nonexistent(void) 208 + { 209 + fprintf(stderr, "== test_swap_nonexistent ==\n"); 210 + 211 + char fileA[512], fileB[512]; 212 + snprintf(fileA, sizeof(fileA), "%s/swap_exist", test_dir); 213 + snprintf(fileB, sizeof(fileB), "%s/swap_noexist", test_dir); 214 + 215 + ASSERT(write_file(fileA, "exists") == 0, "create file A"); 216 + unlink(fileB); /* ensure B does not exist */ 217 + 218 + int ret = renameatx_np(AT_FDCWD, fileA, AT_FDCWD, fileB, RENAME_SWAP); 219 + ASSERT(ret != 0, 220 + "RENAME_SWAP fails when one file does not exist"); 221 + ASSERT(errno == ENOENT, "errno is ENOENT"); 222 + 223 + /* A should be unchanged */ 224 + char buf[64]; 225 + ASSERT(read_file(fileA, buf, sizeof(buf)) == 0, 226 + "file A still readable after failed swap"); 227 + ASSERT(strcmp(buf, "exists") == 0, 228 + "file A content unchanged after failed swap"); 229 + 230 + unlink(fileA); 231 + return 0; 232 + } 233 + 234 + /* ------------------------------------------------------------------ */ 235 + 236 + int main(void) 237 + { 238 + /* Create temporary directory */ 239 + strncpy(test_dir, TEST_DIR_TEMPLATE, sizeof(test_dir)); 240 + if (!mkdtemp(test_dir)) { 241 + perror("mkdtemp"); 242 + return 1; 243 + } 244 + 245 + fprintf(stderr, "Test dir: %s\n\n", test_dir); 246 + 247 + int failures = 0; 248 + failures += test_plain_rename(); 249 + failures += test_rename_swap(); 250 + failures += test_rename_excl(); 251 + failures += test_invalid_flags(); 252 + failures += test_swap_nonexistent(); 253 + 254 + cleanup(); 255 + 256 + fprintf(stderr, "\n%d/%d tests passed\n", tests_passed, tests_run); 257 + if (failures > 0) { 258 + fprintf(stderr, "SOME TESTS FAILED\n"); 259 + return 1; 260 + } 261 + fprintf(stderr, "ALL TESTS PASSED\n"); 262 + return 0; 263 + }
+431
tests/syscall/test_setattrlist_flags.c
··· 1 + /* 2 + * test_setattrlist_flags.c — Regression tests for setattrlist/getattrlist 3 + * with ATTR_CMN_FLAGS support. 4 + * 5 + * This is the core blocker for Nix inside Darling: nix-env calls 6 + * lchflags(path, 0) which decomposes into setattrlist() with 7 + * ATTR_CMN_FLAGS. Previously this returned EINVAL because the flag 8 + * was not in COMMON_SUPPORTED. 9 + * 10 + * Build inside darling shell: 11 + * cc -o test_setattrlist_flags test_setattrlist_flags.c 12 + * 13 + * Run: 14 + * ./test_setattrlist_flags 15 + * 16 + * Exit code 0 = all tests passed, nonzero = failure. 17 + */ 18 + 19 + #include <stdio.h> 20 + #include <stdlib.h> 21 + #include <string.h> 22 + #include <errno.h> 23 + #include <fcntl.h> 24 + #include <unistd.h> 25 + #include <sys/attr.h> 26 + #include <sys/stat.h> 27 + 28 + static int tests_run = 0; 29 + static int tests_passed = 0; 30 + 31 + #define TEST_DIR_TEMPLATE "/tmp/test_setattrlist_XXXXXX" 32 + 33 + static char test_dir[256]; 34 + 35 + static void cleanup(void) 36 + { 37 + char cmd[512]; 38 + snprintf(cmd, sizeof(cmd), "rm -rf %s", test_dir); 39 + system(cmd); 40 + } 41 + 42 + static int write_file(const char *path, const char *content) 43 + { 44 + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); 45 + if (fd < 0) 46 + return -1; 47 + ssize_t len = strlen(content); 48 + ssize_t n = write(fd, content, len); 49 + close(fd); 50 + return (n == len) ? 0 : -1; 51 + } 52 + 53 + #define ASSERT(cond, msg) \ 54 + do { \ 55 + tests_run++; \ 56 + if (!(cond)) { \ 57 + fprintf(stderr, " FAIL [%d]: %s (errno=%d: %s)\n", \ 58 + tests_run, msg, errno, strerror(errno)); \ 59 + return 1; \ 60 + } \ 61 + tests_passed++; \ 62 + fprintf(stderr, " PASS [%d]: %s\n", tests_run, msg); \ 63 + } while (0) 64 + 65 + /* ------------------------------------------------------------------ */ 66 + 67 + /* 68 + * Test 1: setattrlist with ATTR_CMN_FLAGS = 0 69 + * 70 + * This is the exact pattern used by Nix's lchflags(path, 0): 71 + * 72 + * struct attrlist alist = { .bitmapcount = ATTR_BIT_MAP_COUNT, 73 + * .commonattr = ATTR_CMN_FLAGS }; 74 + * uint32_t flags = 0; 75 + * setattrlist(path, &alist, &flags, sizeof(flags), FSOPT_NOFOLLOW); 76 + * 77 + * Before our fix this returned EINVAL; now it must return 0. 78 + */ 79 + static int test_setattrlist_clear_flags(void) 80 + { 81 + fprintf(stderr, "== test_setattrlist_clear_flags ==\n"); 82 + 83 + char path[512]; 84 + snprintf(path, sizeof(path), "%s/clearflags", test_dir); 85 + ASSERT(write_file(path, "test") == 0, "create test file"); 86 + 87 + struct attrlist alist; 88 + memset(&alist, 0, sizeof(alist)); 89 + alist.bitmapcount = ATTR_BIT_MAP_COUNT; 90 + alist.commonattr = ATTR_CMN_FLAGS; 91 + 92 + uint32_t flags = 0; 93 + int ret = setattrlist(path, &alist, &flags, sizeof(flags), FSOPT_NOFOLLOW); 94 + ASSERT(ret == 0, "setattrlist(ATTR_CMN_FLAGS=0) returns 0 (clear flags)"); 95 + 96 + unlink(path); 97 + return 0; 98 + } 99 + 100 + /* 101 + * Test 2: setattrlist with ATTR_CMN_FLAGS = nonzero 102 + * 103 + * Setting nonzero flags (e.g., UF_IMMUTABLE) should also succeed 104 + * (silently ignored) rather than crash or return EINVAL. The key 105 + * contract is that the syscall doesn't reject the attribute. 106 + */ 107 + static int test_setattrlist_nonzero_flags(void) 108 + { 109 + fprintf(stderr, "== test_setattrlist_nonzero_flags ==\n"); 110 + 111 + char path[512]; 112 + snprintf(path, sizeof(path), "%s/nonzeroflags", test_dir); 113 + ASSERT(write_file(path, "test") == 0, "create test file"); 114 + 115 + struct attrlist alist; 116 + memset(&alist, 0, sizeof(alist)); 117 + alist.bitmapcount = ATTR_BIT_MAP_COUNT; 118 + alist.commonattr = ATTR_CMN_FLAGS; 119 + 120 + /* UF_IMMUTABLE = 0x00000002 on macOS */ 121 + uint32_t flags = 0x00000002; 122 + int ret = setattrlist(path, &alist, &flags, sizeof(flags), FSOPT_NOFOLLOW); 123 + /* We accept either success (silently ignored) or ENOTSUP (fs doesn't 124 + * support flags). Both are fine for Nix. The important thing is that 125 + * it does NOT return EINVAL. */ 126 + ASSERT(ret == 0 || errno == ENOTSUP, 127 + "setattrlist(ATTR_CMN_FLAGS=UF_IMMUTABLE) does not return EINVAL"); 128 + 129 + unlink(path); 130 + return 0; 131 + } 132 + 133 + /* 134 + * Test 3: getattrlist with ATTR_CMN_FLAGS 135 + * 136 + * Reading flags should return a buffer with flags == 0 (our stub 137 + * always returns 0 since Linux doesn't track macOS file flags). 138 + */ 139 + static int test_getattrlist_flags(void) 140 + { 141 + fprintf(stderr, "== test_getattrlist_flags ==\n"); 142 + 143 + char path[512]; 144 + snprintf(path, sizeof(path), "%s/getflags", test_dir); 145 + ASSERT(write_file(path, "test") == 0, "create test file"); 146 + 147 + struct attrlist alist; 148 + memset(&alist, 0, sizeof(alist)); 149 + alist.bitmapcount = ATTR_BIT_MAP_COUNT; 150 + alist.commonattr = ATTR_CMN_FLAGS; 151 + 152 + /* 153 + * getattrlist returns: 154 + * uint32_t length; (total size of returned data) 155 + * uint32_t flags; (the ATTR_CMN_FLAGS value) 156 + */ 157 + struct __attribute__((packed)) { 158 + uint32_t length; 159 + uint32_t flags; 160 + } buf; 161 + memset(&buf, 0xFF, sizeof(buf)); /* fill with sentinel */ 162 + 163 + int ret = getattrlist(path, &alist, &buf, sizeof(buf), FSOPT_NOFOLLOW); 164 + ASSERT(ret == 0, "getattrlist(ATTR_CMN_FLAGS) returns 0"); 165 + ASSERT(buf.flags == 0, 166 + "getattrlist reports flags == 0 (no macOS flags on Linux)"); 167 + 168 + unlink(path); 169 + return 0; 170 + } 171 + 172 + /* 173 + * Test 4: Read-modify-write cycle (getattrlist then setattrlist) 174 + * 175 + * This is what many macOS programs do: 176 + * 1. Read current flags via getattrlist 177 + * 2. Modify a flag bit 178 + * 3. Write back via setattrlist 179 + * 180 + * Must not crash or return EINVAL at any step. 181 + */ 182 + static int test_read_modify_write(void) 183 + { 184 + fprintf(stderr, "== test_read_modify_write ==\n"); 185 + 186 + char path[512]; 187 + snprintf(path, sizeof(path), "%s/rmw", test_dir); 188 + ASSERT(write_file(path, "test") == 0, "create test file"); 189 + 190 + /* Step 1: Read flags */ 191 + struct attrlist alist; 192 + memset(&alist, 0, sizeof(alist)); 193 + alist.bitmapcount = ATTR_BIT_MAP_COUNT; 194 + alist.commonattr = ATTR_CMN_FLAGS; 195 + 196 + struct __attribute__((packed)) { 197 + uint32_t length; 198 + uint32_t flags; 199 + } buf; 200 + memset(&buf, 0, sizeof(buf)); 201 + 202 + int ret = getattrlist(path, &alist, &buf, sizeof(buf), FSOPT_NOFOLLOW); 203 + ASSERT(ret == 0, "read: getattrlist succeeds"); 204 + 205 + /* Step 2: Clear all flags (the Nix pattern) */ 206 + uint32_t new_flags = buf.flags & ~0x00000002; /* clear UF_IMMUTABLE */ 207 + ret = setattrlist(path, &alist, &new_flags, sizeof(new_flags), FSOPT_NOFOLLOW); 208 + ASSERT(ret == 0, "write: setattrlist with modified flags succeeds"); 209 + 210 + /* Step 3: Verify by reading again */ 211 + memset(&buf, 0xFF, sizeof(buf)); 212 + ret = getattrlist(path, &alist, &buf, sizeof(buf), FSOPT_NOFOLLOW); 213 + ASSERT(ret == 0, "verify: getattrlist succeeds"); 214 + ASSERT(buf.flags == 0, "verify: flags are still 0"); 215 + 216 + unlink(path); 217 + return 0; 218 + } 219 + 220 + /* 221 + * Test 5: lchflags(path, 0) — the actual libc function 222 + * 223 + * On macOS, lchflags is defined as: 224 + * struct attrlist a = { ATTR_BIT_MAP_COUNT, 0, ATTR_CMN_FLAGS, ... }; 225 + * return setattrlist(path, &a, &flags, sizeof(flags), FSOPT_NOFOLLOW); 226 + * 227 + * This test calls the libc wrapper directly. 228 + */ 229 + static int test_lchflags_zero(void) 230 + { 231 + fprintf(stderr, "== test_lchflags_zero ==\n"); 232 + 233 + char path[512]; 234 + snprintf(path, sizeof(path), "%s/lchflags_test", test_dir); 235 + ASSERT(write_file(path, "test") == 0, "create test file"); 236 + 237 + int ret = lchflags(path, 0); 238 + ASSERT(ret == 0, "lchflags(path, 0) returns 0"); 239 + 240 + unlink(path); 241 + return 0; 242 + } 243 + 244 + /* 245 + * Test 6: chflags(path, 0) — follows symlinks variant 246 + */ 247 + static int test_chflags_zero(void) 248 + { 249 + fprintf(stderr, "== test_chflags_zero ==\n"); 250 + 251 + char path[512]; 252 + snprintf(path, sizeof(path), "%s/chflags_test", test_dir); 253 + ASSERT(write_file(path, "test") == 0, "create test file"); 254 + 255 + int ret = chflags(path, 0); 256 + ASSERT(ret == 0, "chflags(path, 0) returns 0"); 257 + 258 + unlink(path); 259 + return 0; 260 + } 261 + 262 + /* 263 + * Test 7: setattrlist with ATTR_CMN_FLAGS on a symlink (FSOPT_NOFOLLOW) 264 + * 265 + * Nix commonly calls lchflags on symlinks too. This must not crash. 266 + */ 267 + static int test_setattrlist_flags_on_symlink(void) 268 + { 269 + fprintf(stderr, "== test_setattrlist_flags_on_symlink ==\n"); 270 + 271 + char target[512], link_path[512]; 272 + snprintf(target, sizeof(target), "%s/symtarget", test_dir); 273 + snprintf(link_path, sizeof(link_path), "%s/symlink", test_dir); 274 + 275 + ASSERT(write_file(target, "target") == 0, "create symlink target"); 276 + ASSERT(symlink(target, link_path) == 0, "create symlink"); 277 + 278 + struct attrlist alist; 279 + memset(&alist, 0, sizeof(alist)); 280 + alist.bitmapcount = ATTR_BIT_MAP_COUNT; 281 + alist.commonattr = ATTR_CMN_FLAGS; 282 + 283 + uint32_t flags = 0; 284 + int ret = setattrlist(link_path, &alist, &flags, sizeof(flags), FSOPT_NOFOLLOW); 285 + ASSERT(ret == 0, "setattrlist(ATTR_CMN_FLAGS=0) on symlink with NOFOLLOW succeeds"); 286 + 287 + unlink(link_path); 288 + unlink(target); 289 + return 0; 290 + } 291 + 292 + /* 293 + * Test 8: setattrlist with multiple attributes including ATTR_CMN_FLAGS 294 + * 295 + * When ATTR_CMN_FLAGS is combined with other attrs (e.g., ATTR_CMN_MODTIME), 296 + * the buffer layout must be parsed correctly. The flags value comes after 297 + * the time values in the attribute buffer. 298 + */ 299 + static int test_setattrlist_combined_attrs(void) 300 + { 301 + fprintf(stderr, "== test_setattrlist_combined_attrs ==\n"); 302 + 303 + char path[512]; 304 + snprintf(path, sizeof(path), "%s/combined", test_dir); 305 + ASSERT(write_file(path, "combined") == 0, "create test file"); 306 + 307 + struct attrlist alist; 308 + memset(&alist, 0, sizeof(alist)); 309 + alist.bitmapcount = ATTR_BIT_MAP_COUNT; 310 + alist.commonattr = ATTR_CMN_MODTIME | ATTR_CMN_FLAGS; 311 + 312 + /* 313 + * Buffer layout for ATTR_CMN_MODTIME | ATTR_CMN_FLAGS: 314 + * struct timespec modtime; 315 + * uint32_t flags; 316 + */ 317 + struct __attribute__((packed)) { 318 + struct timespec modtime; 319 + uint32_t flags; 320 + } buf; 321 + 322 + buf.modtime.tv_sec = 1700000000; /* some timestamp */ 323 + buf.modtime.tv_nsec = 0; 324 + buf.flags = 0; 325 + 326 + int ret = setattrlist(path, &alist, &buf, sizeof(buf), 0); 327 + ASSERT(ret == 0, 328 + "setattrlist(MODTIME|FLAGS) with combined attrs succeeds"); 329 + 330 + /* Verify the modtime was actually set */ 331 + struct stat st; 332 + ASSERT(stat(path, &st) == 0, "stat after setattrlist succeeds"); 333 + ASSERT(st.st_mtime == 1700000000, 334 + "modification time was set correctly"); 335 + 336 + unlink(path); 337 + return 0; 338 + } 339 + 340 + /* 341 + * Test 9: fsetattrlist with ATTR_CMN_FLAGS via file descriptor 342 + */ 343 + static int test_fsetattrlist_flags(void) 344 + { 345 + fprintf(stderr, "== test_fsetattrlist_flags ==\n"); 346 + 347 + char path[512]; 348 + snprintf(path, sizeof(path), "%s/fset_flags", test_dir); 349 + ASSERT(write_file(path, "test") == 0, "create test file"); 350 + 351 + int fd = open(path, O_RDONLY); 352 + ASSERT(fd >= 0, "open test file"); 353 + 354 + struct attrlist alist; 355 + memset(&alist, 0, sizeof(alist)); 356 + alist.bitmapcount = ATTR_BIT_MAP_COUNT; 357 + alist.commonattr = ATTR_CMN_FLAGS; 358 + 359 + uint32_t flags = 0; 360 + int ret = fsetattrlist(fd, &alist, &flags, sizeof(flags), 0); 361 + ASSERT(ret == 0, "fsetattrlist(ATTR_CMN_FLAGS=0) returns 0"); 362 + 363 + close(fd); 364 + unlink(path); 365 + return 0; 366 + } 367 + 368 + /* 369 + * Test 10: Verify setattrlist rejects truly unsupported attributes 370 + * 371 + * Sanity check that we haven't accidentally accepted everything — attrs 372 + * that are genuinely not supported should still return EINVAL. 373 + */ 374 + static int test_setattrlist_rejects_unsupported(void) 375 + { 376 + fprintf(stderr, "== test_setattrlist_rejects_unsupported ==\n"); 377 + 378 + char path[512]; 379 + snprintf(path, sizeof(path), "%s/unsupported", test_dir); 380 + ASSERT(write_file(path, "test") == 0, "create test file"); 381 + 382 + struct attrlist alist; 383 + memset(&alist, 0, sizeof(alist)); 384 + alist.bitmapcount = ATTR_BIT_MAP_COUNT; 385 + /* ATTR_CMN_NAME = 0x00000001 — should not be settable */ 386 + alist.commonattr = 0x00000001; 387 + 388 + uint32_t dummy = 0; 389 + int ret = setattrlist(path, &alist, &dummy, sizeof(dummy), 0); 390 + ASSERT(ret != 0, "setattrlist rejects unsupported ATTR_CMN_NAME"); 391 + ASSERT(errno == EINVAL, "errno is EINVAL for unsupported attribute"); 392 + 393 + unlink(path); 394 + return 0; 395 + } 396 + 397 + /* ------------------------------------------------------------------ */ 398 + 399 + int main(void) 400 + { 401 + /* Create temporary directory */ 402 + strncpy(test_dir, TEST_DIR_TEMPLATE, sizeof(test_dir)); 403 + if (!mkdtemp(test_dir)) { 404 + perror("mkdtemp"); 405 + return 1; 406 + } 407 + 408 + fprintf(stderr, "Test dir: %s\n\n", test_dir); 409 + 410 + int failures = 0; 411 + failures += test_setattrlist_clear_flags(); 412 + failures += test_setattrlist_nonzero_flags(); 413 + failures += test_getattrlist_flags(); 414 + failures += test_read_modify_write(); 415 + failures += test_lchflags_zero(); 416 + failures += test_chflags_zero(); 417 + failures += test_setattrlist_flags_on_symlink(); 418 + failures += test_setattrlist_combined_attrs(); 419 + failures += test_fsetattrlist_flags(); 420 + failures += test_setattrlist_rejects_unsupported(); 421 + 422 + cleanup(); 423 + 424 + fprintf(stderr, "\n%d/%d tests passed\n", tests_passed, tests_run); 425 + if (failures > 0) { 426 + fprintf(stderr, "SOME TESTS FAILED\n"); 427 + return 1; 428 + } 429 + fprintf(stderr, "ALL TESTS PASSED\n"); 430 + return 0; 431 + }