diff --git a/.github/workflows/grease-receipt.yml b/.github/workflows/grease-receipt.yml index 08dbc07..c0d3570 100644 --- a/.github/workflows/grease-receipt.yml +++ b/.github/workflows/grease-receipt.yml @@ -5,16 +5,22 @@ on: push: branches: [main] +permissions: + contents: read + jobs: cpp-spec: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 + timeout-minutes: 120 env: REPO_ROOT: ${{ github.workspace }}/source-worktree steps: - - name: Check out Grease and pinned Oils source + - name: Check out exact Grease head and pinned Oils source uses: actions/checkout@v4 with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} submodules: recursive + persist-credentials: false - name: Verify and record the source pin id: source_pin @@ -22,7 +28,13 @@ jobs: run: | revision=$(git rev-parse HEAD:source) test "$(git -C source rev-parse HEAD)" = "$revision" + . /etc/os-release + test "$ID" = ubuntu + test "$(uname -m)" = x86_64 echo "revision=$revision" >> "$GITHUB_OUTPUT" + echo "Grease: $(git rev-parse HEAD)" + echo "Oils source: $revision" + echo "Ubuntu: $PRETTY_NAME" # The inherited container harness mounts an Oils worktree by itself. # A normal submodule has a .git file whose target is outside that mount, @@ -35,17 +47,44 @@ jobs: path: source-worktree persist-credentials: false - - name: Verify the standalone source revision + - name: Verify standalone source revision shell: bash - run: | - test "$(git -C source-worktree rev-parse HEAD)" = "${{ steps.source_pin.outputs.revision }}" + run: test "$(git -C source-worktree rev-parse HEAD)" = "${{ steps.source_pin.outputs.revision }}" - name: Fix kernel mmap rnd bits run: sudo sysctl vm.mmap_rnd_bits=28 - - name: Run and verify the inherited Oils cpp-spec oracle + - name: Run and verify inherited Oils cpp-spec oracle working-directory: source-worktree + shell: bash run: | + podman --version soil/github-actions.sh run-job cpp-spec podman test "$(cat _tmp/soil/commit-hash.txt)" = "${{ steps.source_pin.outputs.revision }}" soil/host-shim.sh did-all-succeed cpp-spec + + - name: Install host dependency for focused native test + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends libreadline-dev + + - name: Build and run focused libc native tests + working-directory: source-worktree + shell: bash + run: | + sudo chown -R "$(id -u):$(id -g)" _build _bin + ninja _bin/cxx-dbg/cpp/libc_test + _bin/cxx-dbg/cpp/libc_test -t grease_native_ + + - name: Exercise mapped IB index from Grease + working-directory: source-worktree + shell: bash + run: | + test -x _bin/cxx-asan/ysh + index_file=$(mktemp) + trap 'rm -f "$index_file"' EXIT + truncate -s 4096 "$index_file" + printf fragment | dd of="$index_file" conv=notrunc status=none + _bin/cxx-asan/ysh \ + "$GITHUB_WORKSPACE/examples/ib-mapped-index.ysh" "$index_file" + test "$(head -c 8 "$index_file")" = pensieve diff --git a/docs/NATIVE-LIBC.md b/docs/NATIVE-LIBC.md new file mode 100644 index 0000000..0143d43 --- /dev/null +++ b/docs/NATIVE-LIBC.md @@ -0,0 +1,155 @@ +# Native libc actions in Grease + +Grease is still the Oils/YSH-derived executable described in +[`CURRENT-GREASE.md`](CURRENT-GREASE.md). This native vocabulary extends that +implementation; it does not add a second runtime. + +The C++ layer here is inherited from Oils. Oils translates the shell runtime to +C++ for the native `oils-for-unix` / YSH executable, and already keeps native +replacements for Python-extension operations in `cpp/`. Grease therefore uses +that existing seam: + +```text +Grease / YSH program + | + v +native object in builtin/func_native.py + | + v +pyext/libc.pyi boundary + | + v +existing Oils cpp/libc.h + cpp/libc.cc native layer + | + v +libc (Bionic on Android) -> kernel +``` + +There is no raw syscall-number table and no general C FFI. Grease does not get +`dlsym`, arbitrary pointer calls, variadic calls, callbacks, or general struct +marshalling from this work. + +## Public actions + +The built-in `native` object exposes these operating-system actions: + +- `mmap` +- `munmap` +- `mprotect` +- `msync` +- `openat` +- `linkat` +- `symlinkat` +- `unlinkat` + +Three small support actions make those resources usable without exposing raw C +values: + +- `close` closes a descriptor returned by `openat`; +- `readMapping` reads a bounded byte range from a live mapping; +- `writeMapping` writes a bounded byte range to a live mapping. + +`native.cwd` is a borrowed directory descriptor representing `AT_FDCWD`. It can +be used as the directory argument to the `*at` actions, but it cannot be closed. + +## Resource values + +The public language boundary does not expose a C pointer or kernel file +descriptor as an integer. + +`mmap` returns a `Mapping` object. A mapping contains an opaque `Address`, its +mapped length, and live/inactive state. The native C++ registry owns the actual +`void *` and length. `munmap`, `mprotect`, `msync`, `readMapping`, and +`writeMapping` resolve the opaque handle through that registry. + +`openat` returns a `FileDescriptor` object. Its opaque token resolves to a real +file descriptor in a separate native registry. The object records whether it is +a directory descriptor, whether it is borrowed, and whether it has been closed. + +These are runtime distinctions in current Grease/YSH, not a claim that YSH has +a new static nominal type system. Lengths, offsets, and modes use the existing +YSH `Int` value at the language surface. Lengths and offsets cross the native +boundary as decimal text and are range-checked before conversion to `size_t` or +`off_t`, avoiding an accidental narrowing through a C `int`. + +## Errors + +Native calls return a Grease result object rather than requiring code to inspect +C sentinels such as `-1` or `MAP_FAILED`. + +A result has: + +- `ok`: boolean success status; +- `value`: the result value on success; +- `error`: a `NativeError` on failure. + +A `NativeError` contains the errno number, its symbolic name when known, and the +platform error message. A failed `mmap` never becomes a usable `Address`. + +## Flags + +Grease uses stable text names instead of exporting libc's numeric constants. +The C++ boundary translates those names' internal bit masks to the constants of +the platform being built. + +Memory protection names are `none`, `read`, `write`, and `execute`. +Mapping names are `private`, `shared`, and `anonymous`; exactly one of `private` +or `shared` is required. Synchronization names are `sync`, `async`, and +`invalidate`; exactly one of `sync` or `async` is required. + +`openat` accepts `read-only`, `write-only`, `read-write`, `create`, `exclusive`, +`truncate`, `append`, `directory`, `no-follow`, and `close-on-exec`, with exactly +one access mode. `linkat` has a `followSymlink` boolean and `unlinkat` has a +`removeDirectory` boolean. + +## Implementation boundary + +The native implementation calls libc directly: + +```text +mmap munmap mprotect msync +openat close linkat symlinkat unlinkat +``` + +On Android these resolve through Bionic. The current phone build targets Android +API 28, which is above the API boundary needed by the selected `*at` and mapping +interfaces. No ARM syscall numbers are part of the Grease API or implementation. + +The Python reference execution path is not claimed as an implementation of +these actions. `pyext/libc.pyi` describes the translated boundary, while the +actual new operations are supplied by `cpp/libc.cc` in the native Grease/Oils +build. A Python-only YSH run can construct the `native` object but is not an +acceptance target for invoking these new actions. + +## Relationship to ish + +`ish` is the separate OdriƧ successor line described in `CURRENT-GREASE.md`. +Its direct `execve` milestone is useful design evidence, but it is not the +runtime underneath current Grease. This work therefore does not manufacture a +second `ish` bridge merely to duplicate the Oils native boundary. + +Existing Oils process execution remains inherited behavior and is checked by +the normal native shell test suite. + +## Representative IB use + +[`../examples/ib-mapped-index.ysh`](../examples/ib-mapped-index.ysh) is the +small end-to-end proof. It opens a prepared 4096-byte index file, maps it shared, +reads `fragment`, writes `pensieve`, synchronizes the mapping, reads the changed +value back, unmaps it, and closes the descriptor. The receipt then checks the +file bytes outside the Grease process to prove that the shared mapping really +persisted the change. + +The focused C++ tests additionally cover anonymous mappings, protection changes, +out-of-bounds access, deliberate error cases, file-backed mappings, hard links, +symbolic links, relative `openat`, and `unlinkat`. + +## Extending the vocabulary + +A later native action should stay explicit and small. Add its declaration to the +translated libc boundary, implement the libc/Bionic call in `cpp/libc.cc`, add a +Grease method that converts readable values to that boundary, and exercise both +success and error behavior. If this repeated shape grows large enough to justify +a declaration-driven generator, that can be introduced from demonstrated +repetition rather than turning this first vocabulary into a general-purpose +FFI. diff --git a/examples/ib-mapped-index.ysh b/examples/ib-mapped-index.ysh new file mode 100644 index 0000000..bc1f448 --- /dev/null +++ b/examples/ib-mapped-index.ysh @@ -0,0 +1,63 @@ +#!/usr/bin/env ysh + +# Representative IB/Pensieve proof: work directly with a mapped index page. +# The acceptance workflow prepares a 4096-byte file whose first eight bytes are +# "fragment" and verifies that this program persists "pensieve" through mmap. + +if (len(ARGV) !== 1) { + echo 'usage: ib-mapped-index.ysh INDEX_FILE' + exit 2 +} + +var opened = native.openat(native.cwd, ARGV[0], flags=['read-write']) +if (not opened.ok) { + echo 'openat failed' + exit 3 +} +var index_file = opened.value + +var mapped = native.mmap(4096, index_file, + protection=['read', 'write'], flags=['shared']) +if (not mapped.ok) { + echo 'mmap failed' + exit 4 +} +var index_page = mapped.value + +var before = native.readMapping(index_page, 0, 8) +if (not before.ok or before.value !== 'fragment') { + echo 'mapped input did not contain fragment' + exit 5 +} + +var wrote = native.writeMapping(index_page, 0, 'pensieve') +if (not wrote.ok) { + echo 'mapped write failed' + exit 6 +} + +var synced = native.msync(index_page, flags=['sync']) +if (not synced.ok) { + echo 'msync failed' + exit 7 +} + +var after = native.readMapping(index_page, 0, 8) +if (not after.ok or after.value !== 'pensieve') { + echo 'mapped value did not change to pensieve' + exit 8 +} + +var unmapped = native.munmap(index_page) +if (not unmapped.ok) { + echo 'munmap failed' + exit 9 +} + +var closed = native.close(index_file) +if (not closed.ok) { + echo 'close failed' + exit 10 +} + +echo 'IB mapped-index proof passed' diff --git a/phone/build-armv7-runtime.sh b/phone/build-armv7-runtime.sh index 29dca55..66b66a4 100644 --- a/phone/build-armv7-runtime.sh +++ b/phone/build-armv7-runtime.sh @@ -106,12 +106,12 @@ runtime=_bin/android-armv7-clang++-opt-sh/oils-for-unix.stripped exit 3 } -"$readelf" -h "$runtime" | grep -Eq 'Machine:[[:space:]]+ARM' || { +"$readelf" -h "$runtime" | grep -E 'Machine:[[:space:]]+ARM' >/dev/null || { echo 'Grease phone runtime is not an ARM ELF binary' >&2 "$readelf" -h "$runtime" >&2 exit 3 } -if "$readelf" -d "$runtime" 2>/dev/null | grep -q 'libc++_shared\.so'; then +if "$readelf" -d "$runtime" 2>/dev/null | grep 'libc++_shared\.so' >/dev/null; then echo 'Grease phone runtime unexpectedly depends on libc++_shared.so' >&2 exit 3 fi diff --git a/source b/source index 8052868..9024d14 160000 --- a/source +++ b/source @@ -1 +1 @@ -Subproject commit 8052868773077602266d80bf39aad6998e2da749 +Subproject commit 9024d14ecd25c8c55cd7e8ea0b901a3a9525565c