Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
dab0d07
Pin Grease native libc vocabulary source
isomorphisms Sep 10, 2026
f0b27ae
ci: expose native diagnostics and run libc test
isomorphisms Sep 10, 2026
cb40194
Pin current native libc source and diagnostics
isomorphisms Sep 10, 2026
26e79e3
native CI: repair translated bridge and host build handoff
isomorphisms Sep 11, 2026
efa6342
native CI: run only focused libc bridge tests
isomorphisms Sep 11, 2026
3547222
Pin translator-safe native libc boundary
isomorphisms Sep 11, 2026
983120a
Pin scoped native libc implementation
isomorphisms Sep 11, 2026
dfa8f58
Exercise file-backed mapping from Grease
isomorphisms Sep 11, 2026
3e61bf3
Exercise native actions from Grease syntax
isomorphisms Sep 11, 2026
bfe9f10
Document the narrow native libc boundary
isomorphisms Sep 11, 2026
24e5ebc
Pin protection-aware native mappings
isomorphisms Sep 11, 2026
dc78e1e
Use call-site syntax for native named arguments
isomorphisms Sep 11, 2026
c8b2c70
ci: run Oils cpp-spec natively on Debian
isomorphisms Sep 11, 2026
c66220f
ci: build ARMv7 runtime natively from Debian
isomorphisms Sep 11, 2026
73eaea6
ci: make Debian ARMv7 dependencies explicit
isomorphisms Sep 11, 2026
2d59688
ci: keep fork PRs off Debian receipt runner
isomorphisms Sep 11, 2026
958b342
ci: keep fork PRs off Debian ARMv7 runner
isomorphisms Sep 11, 2026
aee2364
ci: return native libc receipt to hosted Ubuntu
isomorphisms Sep 14, 2026
30d14fb
ci: return ARMv7 build to hosted Ubuntu
isomorphisms Sep 14, 2026
5877522
Merge phone/armv7-runtime into native Linux libc vocabulary
isomorphisms Sep 14, 2026
8e5a562
Fix ARM ELF receipt checks under pipefail
isomorphisms Sep 14, 2026
9985a12
Provision readline for hosted libc receipt
isomorphisms Sep 14, 2026
758f9ad
Reuse cpp-spec YSH for mapped IB receipt
isomorphisms Sep 14, 2026
ef9c740
Reconcile ARM runtime verifier with base
isomorphisms Sep 16, 2026
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
51 changes: 45 additions & 6 deletions .github/workflows/grease-receipt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,36 @@ 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
shell: bash
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,
Expand All @@ -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
155 changes: 155 additions & 0 deletions docs/NATIVE-LIBC.md
Original file line number Diff line number Diff line change
@@ -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.
63 changes: 63 additions & 0 deletions examples/ib-mapped-index.ysh
Original file line number Diff line number Diff line change
@@ -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'
4 changes: 2 additions & 2 deletions phone/build-armv7-runtime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion source
Loading