From c43ecd5f135b914f3d2c4502c910b603b001b647 Mon Sep 17 00:00:00 2001 From: Nemo <328747105+Nemo-010@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:31:47 +0000 Subject: [PATCH 1/2] Skip a wrong-class candidate in the GL host lookup glfwd_try_soname accepted the first directory in which its own soname existed, and glfwd_each_dir stopped the walk there. A candidate of the other ELF class cannot load, so a host that named a 32bit directory first produced a target the loader refused and the directory after it was never reached. The check compares EI_CLASS against sizeof(void *), so it is the same predicate on every target this project builds. Measured with the five-name miniature of experiments/30-run-tests.sh section O. Built from the pre-fix source, a decoy libtgt.so carrying the other EI_CLASS in the first named directory makes the shim report "no target; all 5 entry points return zero". With the repair it reports the libtgt.so in the directory named second and the probe passes. The same fixture with the real library first still resolves, and a candidate that is not an ELF is still handed to dlopen as before, which is why the predicate is class-specific. E103 is the suite case. docs/report/09-the-second-boundary.md 9.21 has the transcript, and the evidence totals move to 66/66 on x86-64 and 62/62 on aarch64. Refs #40. --- .github/workflows/gates.yml | 2 +- docs/report/01-summary.md | 2 +- docs/report/08-test-results.md | 9 +- docs/report/09-the-second-boundary.md | 39 ++++++- docs/report/10-measured-versus-assumed.md | 2 +- docs/todo/PROGRESS.md | 121 +++++++--------------- experiments/30-run-tests.sh | 20 +++- scripts/verify-gates.sh | 4 +- src/gl-fwd.c | 18 +++- 9 files changed, 123 insertions(+), 94 deletions(-) diff --git a/.github/workflows/gates.yml b/.github/workflows/gates.yml index 37b2fc7..beb58bd 100644 --- a/.github/workflows/gates.yml +++ b/.github/workflows/gates.yml @@ -150,7 +150,7 @@ jobs: - name: every headline number has exactly one home run: | fail=0 - for n in 3470 358 65/65 61/61 45/45 40/40 26/26; do + for n in 3470 358 66/66 62/62 45/45 40/40 26/26; do homes=$(git grep -lF "$n" -- '*.md' ':(exclude)docs/history/*' | sed 's|^docs/report/.*|docs/report/|' | sort -u | tr '\n' ' ') count=$(printf '%s' "$homes" | wc -w) diff --git a/docs/report/01-summary.md b/docs/report/01-summary.md index e26d7de..b5d6c03 100644 --- a/docs/report/01-summary.md +++ b/docs/report/01-summary.md @@ -11,7 +11,7 @@ | Completion criterion | Status | |---|---| | Both goals demonstrated by a test that fails before and passes after | **Yes.** Goal 1: E5, E12. Goal 2: E22/E23 for the mechanism, E30/E32 and E37a/E37 for the end-to-end | -| The evidence harness still reports all predictions held | **Yes, 65/65** on x86-64 and **61/61** on aarch64, up from 22/22. The AppImage suite adds 45 on a glvnd glibc host, 40 on musl, 26 on each of two pre-glvnd glibc hosts and 7 on a real-application stage, with every unrunnable case SKIPPED by the capability it lacks | +| The evidence harness still reports all predictions held | **Yes, 66/66** on x86-64 and **62/62** on aarch64, up from 22/22. The AppImage suite adds 45 on a glvnd glibc host, 40 on musl, 26 on each of two pre-glvnd glibc hosts and 7 on a real-application stage, with every unrunnable case SKIPPED by the capability it lacks | | No host file modified, verified by checksum | **Yes.** T4.3, identical sha256 before and after | | Bundled libraries still win, verified via `dladdr` | **Yes.** T4.2, all resolved under `$APPDIR` | | A forward-compatibility story that does not depend on foresight | **Yes.** Host-runtime selection for the unenumerable gap, a generated shim for the enumerable one, and a build-time audit (E26) for the version traps | diff --git a/docs/report/08-test-results.md b/docs/report/08-test-results.md index 977d6e0..fb85a30 100644 --- a/docs/report/08-test-results.md +++ b/docs/report/08-test-results.md @@ -24,9 +24,9 @@ wrong. ### Tier 1, the evidence table -`sh scripts/run-evidence.sh` reports **65/65 predictions held on x86-64** and -**61/61 on aarch64**. The x86-64 total was measured at the change that added -E102; the aarch64 runner runs the same table, so its total is the x86-64 +`sh scripts/run-evidence.sh` reports **66/66 predictions held on x86-64** and +**62/62 on aarch64**. The x86-64 total was measured at the change that added +E103; the aarch64 runner runs the same table, so its total is the x86-64 total minus the four skips below, and CI re-runs both on every push. `experiments/run.ps1` drives the same three stage scripts for a machine with PowerShell and no POSIX shell. @@ -43,7 +43,7 @@ naming the capability it lacks rather than the difference being unexplained: ⭐ **E23's skip is the one worth reading.** It was reporting MATCH on the ARM runner while asserting nothing, and skipping it with E22 is what stopped that. -65 minus 4 is 61, and no case is missing for a reason nobody wrote down. +66 minus 4 is 62, and no case is missing for a reason nobody wrote down. E1 through E13 measure the problem. E14 through E21 are one per fix from the first pass: the ELF self-test, the generated-shim compile and behaviour, and five selector @@ -75,6 +75,7 @@ objects small enough that the mechanism is the only thing being measured: | E72 | an entry point the target does not provide, CALLED: a line naming it, and zero returned | | E73 | the distinct-name call count an application can be measured by | | E75 / E75b | the shim finds a target in a directory only `/etc/ld.so.conf` names, and does not when the conf file is removed | +| E103 | a directory the host names first holds a wrong-class library: the shim skips it and finds the one that loads, instead of reporting no target | | E75c / E75d / E75e / E75f | a shim with no target serves each name from the provider behind it in the lookup order; the no-preload control; the log line pinning the fallthrough; the same in EAGER mode | | E76 / E76b | the aarch64 trampolines and resolver RUN, under qemu-user, forwarding and absent paths both | diff --git a/docs/report/09-the-second-boundary.md b/docs/report/09-the-second-boundary.md index f074992..28dcbc6 100644 --- a/docs/report/09-the-second-boundary.md +++ b/docs/report/09-the-second-boundary.md @@ -362,7 +362,7 @@ Totals with this section in: **40/40 on the musl host** with five named skips, and ubuntu:16.04 with nineteen named skips, **7/7** on the gtk4 stage, and **53/53** in the container suite on x86-64, and **50/50** on aarch64 with the three skips named in section 8, at the measurement this section records; the -totals for the tree as it ships are **65/65** and **61/61** in section 8. +totals for the tree as it ships are **66/66** and **62/62** in section 8. ### 9.8 What the shim does not do, stated as a number @@ -1496,6 +1496,43 @@ repaired tree holds all of them. --- +### 9.21 The host lookup stopped on a library the loader would refuse + +`glfwd_try_soname` accepted the first directory in which its own soname +existed, and `glfwd_each_dir` stopped the walk there. `ld.so` refuses a +candidate of the other ELF class before it reads anything else, so a host whose +own answer named a 32bit library directory before the 64bit one produced a +target that could not load, and the directory after it was never reached. The +shim reported no target and the application failed with its own message about +visuals. + +Reproduced by removing `libGL.so.1` from the host-drivers demo AppDir, so that +the shim takes the host path instead of the bundled dispatcher, and naming the +32bit directory first with the explicit handoff: + +``` +CROSS_LIBC_DLOPEN_GL_HOST_DIR=/usr/lib/i386-linux-gnu:/usr/lib/x86_64-linux-gnu \ +CROSS_LIBC_DLOPEN_DEBUG=1 ./AppDir/AppRun glxgears + + [cross-libc-dlopen.so] >> cross-libc dlopen failed: /usr/lib/i386-linux-gnu/libGL.so.1 + [gl-fwd.so] >> host /usr/lib/i386-linux-gnu/libGL.so.1 would not load: /usr/lib/i386-linux-gnu/libGL.so.1.7.0: wrong ELF class: ELFCLASS32 + [gl-fwd.so] >> libGL.so.1: no target; 358 of 3470 entry points fall through to the next provider in scope + [gl-fwd.so] >> ABSENT entry point called: glXChooseVisual -- no target on this host and no next provider for it; returning zero +Error: couldn't get an RGB, Double-buffered visual +``` + +The repair is `glfwd_wrong_class`: the candidate's ELF ident is read, and one +that is an ELF of the other class is skipped so the walk continues to the next +directory. A candidate that is not recognisable as an ELF is left to `dlopen` +exactly as before, so nothing that loads today stops being chosen. + +E103 is the case. It uses the five-name miniature of section O, with the +`libtgt.so` in the directory named first carrying the other EI_CLASS. Before the +repair the shim reports `no target`; after it, it reports the `libtgt.so` in the +directory named second and the probe passes. + +--- + --- [REPORT index](README.md) | [previous](08-test-results.md) | [next](10-measured-versus-assumed.md) diff --git a/docs/report/10-measured-versus-assumed.md b/docs/report/10-measured-versus-assumed.md index 5110e7e..cf597f6 100644 --- a/docs/report/10-measured-versus-assumed.md +++ b/docs/report/10-measured-versus-assumed.md @@ -1,7 +1,7 @@ ## 10. Measured versus assumed **Measured:** every table and quoted output above, plus `sh scripts/run-evidence.sh` -(65/65 on x86-64, 61/61 on aarch64), `sh scripts/run-appimage.sh` (45/45 glvnd glibc, 40/40 musl with five +(66/66 on x86-64, 62/62 on aarch64), `sh scripts/run-appimage.sh` (45/45 glvnd glibc, 40/40 musl with five named skips, 26/26 on each pre-glvnd glibc host, 7/7 on the gtk4 stage), `tools/gap.py --fetch`, the eight-distro inventory, the AppImage inventory, the corpus test, the real mpv and `iHD` VA-API run in section 9.19, and the five-distro `ld.so.cache` survey in `../ground-truth.md`. diff --git a/docs/todo/PROGRESS.md b/docs/todo/PROGRESS.md index 4061574..c2aab4c 100644 --- a/docs/todo/PROGRESS.md +++ b/docs/todo/PROGRESS.md @@ -8,104 +8,61 @@ ## Where the work is right now -[v0.2.5](https://github.com/pkgforge-dev/cross-libc-dlopen/releases/tag/v0.2.5) is -published. The aarch64, riscv64 and loongarch64 artefacts of every release so -far carry the defect below; there is no release yet without it. +[v0.2.6](https://github.com/pkgforge-dev/cross-libc-dlopen/releases/tag/v0.2.6) is +published. | workflow | latest on `main` | |---|---| | `gates` | ✅ | | `secret-sweep` | ✅ | -| `release` | ✅ on `v0.2.5` | +| `release` | ✅ on `v0.2.6` | -**This branch fixes issue #37 and needs a release once merged.** The next -consumer (Helium, Brave, Chrome AppImages via quick-sharun) is crashing on -aarch64 CI today. +**This branch fixes the host lookup that stopped on a library the loader would +refuse, on branch `fix/gl-fwd-wrong-class`.** --- ## ⛔ The work order -### 1. `__stack_chk_guard` must not be defined where the loader owns it - -⭐ **This is the session's work, on branch `fix/loader-owned-exports`.** - -The generated shim emitted `__stack_chk_guard` as a function stub, because -the generator consulted the x86-64 target inventory for its type and the name -is musl-only there. On aarch64, riscv64 and loongarch64 the dynamic loader -exports that name as the process-wide stack canary, and an unversioned -preload definition wins every lookup in the process, `libc.so.6`'s own -included. Chromium's GPU process died with SIGSEGV under the preload with the -feature switch off; x86-64 and ppc64le were never affected, because their -loaders keep the canary in thread storage and never export the name. -[../report/03](../report/03-defects-found-by-measurement.md) 3.7 has the -whole chain with the measurement table. - -Three layers, all on this branch: - -- the generator excludes the name on the three architectures whose loader - exports it, emits it as zeroed data of the real size everywhere else, and - decides FUNC versus OBJECT from the merged kind table rather than the x86-64 - target's. `src/forward-shim.c` and the manifest are regenerated. -- `scripts/verify-artifacts.sh` refuses any build whose preload exports a - name the target's own libc family also exports, beyond `dlopen` and - `version-compat.c`'s forwarders. `CLD_SYSROOT` names the target sysroot for - a verification outside a build; that is how its refusal was proven on this - x86-64 machine against the real bullseye arm64 cross libc. -- **E102** is the suite's case, run on both rows of the evidence table. Its - logic was proven against the real floor cross libc on this machine: the - released v0.2.5 aarch64 object makes it report `__stack_chk_guard` and the - fixed one reports nothing. The row on the ARM runner itself is the PR's CI - run, which is where E101's aarch64 number was measured too. - -The suite total moved by one on both rows with E102. Report 08 owns the new -numbers, and every one-home record moved with it: `gates.yml`, -`scripts/verify-gates.sh` and that script's probe string. - -Measured locally besides the suite: the fixed x86-64 build exports the same -140 names as before, with seven musl-object symbols re-typed from FUNC stubs -to data of their real sizes (`___environ`, `__optpos`, `__optreset`, -`__stack_chk_guard`, `_ns_flagdata`, `h_errno`, `optreset`); the planted -defect, the released v0.2.5 aarch64 object, makes both the gate and E102's -logic refuse naming the symbol; the gate's `defined_names` was caught reading -only the first member of the target list, which measured a whole pass against -`libc.so.6` alone and reported the loader's canary absent. - -And measured on real silicon, in a consumer's CI rather than this -repository's: an unmodified Helium AppImage build whose only change is -compiling this branch and dropping the object into `AppDir/lib/sharun-preload` -passes its packaged `--test` on aarch64 and x86-64, with zero GPU-process -SIGSEGVs in both the `CROSS_LIBC_DLOPEN=0` control arm and the default arm, -where the released object had six in each. The runs are linked from the pull -request, because a fork's logs are not files this repository can cite. +### 1. `glfwd_try_soname` must not stop on a library the loader will refuse + +`glfwd_try_soname` took the first directory in which its own soname existed, and +`glfwd_each_dir` stopped the walk there. A candidate of the other ELF class +cannot load, so a host whose own answer named a 32bit library directory first +produced a target that could not load, and the directory after it was never +reached. Reproduced by removing `libGL.so.1` from the host-drivers demo AppDir, +which forces the host path, and naming the 32bit directory first with +`CROSS_LIBC_DLOPEN_GL_HOST_DIR`. + +The repair reads the candidate's ELF ident and skips a candidate of the other +class, so the walk continues. `docs/report/09-the-second-boundary.md` 9.21 has +the transcript, E103 is the case, and `docs/report/08-test-results.md` owns the +new totals. ### 2. What is still open -Nothing from this session. The open list is [`INDEX.md`](INDEX.md) and the -work order lives nowhere else. +`glfwd_try_vendor` accepts a matching filename without checking its class, so a +host whose only vendor library is the wrong class is reported as having one. It +is a different path, reached only when the bundle carries a dispatcher, and it +needs a bundled-dispatcher case to measure. Nothing in this session's change +touches it. --- ## ⚠ What a new session should distrust -- **The aarch64 and riscv64 and loongarch64 rows of `build` in CI are the - only builds that exercise the new gate against a real target sysroot.** A - green row means the artefact exports no name that target's libc family has; - a machine without that libc installed now REFUSES the build rather than - reporting the property unverified, so the three architectures with no - runner cannot ship on a check that measured nothing. -- **`forward-shim.c` is one file compiled for every architecture, and its - arch-conditioned emission is the only per-architecture behaviour in it.** - The `#if` guard spells the three excluded architectures with the same - macros `src/cross-libc-dlopen.c` uses for its triplets. A new architecture - added to the build emits the symbol by default; the gate then refuses the - build if that architecture's loader exports the name, which is the intended - failure rather than a shipped crash. -- **The wide appimage suite has failed at its extraction step since late - August**, on both rows, for reasons unrelated to this branch. Its aarch64 - row is the only real-driver run this repository has, and it is not running. - Fixing that is not this branch's work. -- **`__stack_chk_guard` is load-bearing on x86-64 and ppc64le**, where musl - guests bind it by name. Removing it entirely rather than per architecture - would turn a working musl load into an unresolved strong symbol at `dlopen` - time. E49 covers that path on x86-64 and still passes. +- **E103 forces the order through `/etc/ld.so.conf.d`, and a host whose own + answer puts the 64bit directory first never reaches the defect.** Debian 10 + is such a host: `libc6-i386` names `/usr/lib32` and `/lib32` in + `zz_i386-biarch-compat.conf`, which sorts after `x86_64-linux-gnu.conf`. So + the case pins the predicate, not a claim about any one distribution. +- **The host-drivers demo AppDir bundles the glvnd dispatcher, so the shim + takes the bundled path on a glvnd host and never reaches the host lookup.** + Removing `libGL.so.1` from that AppDir is what exposes the path, and the + deletion is a harness step rather than a change to any shipped AppImage. +- **A Qt6 GLX path does not link `libGL.so.1`.** `libqxcb-glx-integration.so` + and `libQt6Gui.so.6` need `libGLX.so.0` and `libOpenGL.so.0`, so which arm a + Qt application reaches depends on what else in its AppDir links + `libGL.so.1`. Check `readelf -d` before assuming. +- **`docs/todo/INDEX.md` carries the counts it carried before this session.** + Reconcile them if an entry moves. diff --git a/experiments/30-run-tests.sh b/experiments/30-run-tests.sh index 8bdfb4b..f4f5de5 100644 --- a/experiments/30-run-tests.sh +++ b/experiments/30-run-tests.sh @@ -1022,8 +1022,19 @@ echo "-- O. the shim asks the HOST where its libraries are ------------" # using the mechanism's own presence, not an env switch invented to disable it. mkdir -p /opt/cross-libc-unguessable-42 /etc/ld.so.conf.d cp libtgt.so /opt/cross-libc-unguessable-42/libtgt.so +# E103's decoy: the same library with EI_CLASS flipped, so it is an ELF of the +# other class and ld.so refuses it. A genuine ELF32 cannot be built on every +# runner (as --32 is x86 only), and the ident byte is what decides the class on +# all of them. +mkdir -p /opt/cross-libc-wrongclass-41 +cp libtgt.so /opt/cross-libc-wrongclass-41/libtgt.so +printf '\001' | dd of=/opt/cross-libc-wrongclass-41/libtgt.so bs=1 seek=4 conv=notrunc status=none [ -f /etc/ld.so.conf ] || printf 'include /etc/ld.so.conf.d/*.conf\n' > /etc/ld.so.conf confdir() { printf '/opt/cross-libc-unguessable-42\n' > /etc/ld.so.conf.d/zz-cross-libc.conf; } +# The same file with the wrong-class directory named first, which is what a +# host whose own answer orders its 32bit library directory before the 64bit one +# looks like. +confdir_decoy() { printf '/opt/cross-libc-wrongclass-41\n/opt/cross-libc-unguessable-42\n' > /etc/ld.so.conf.d/zz-cross-libc.conf; } noconf() { rm -f /etc/ld.so.conf.d/zz-cross-libc.conf; } # No CROSS_LIBC_DLOPEN_GL_HOST_DIR here: the whole question is whether the shim finds it # without being told, so the handoff that would tell it is left out. @@ -1032,6 +1043,13 @@ fwd_noenv() { env LD_PRELOAD="$PWD/tgt-fwd.so" CROSS_LIBC_DLOPEN_DEBUG=1 "$@"; } # E75: the directory is named ONLY by /etc/ld.so.conf.d, and the shim finds it. confdir run E75 OK "target /opt/cross-libc-unguessable-42/libtgt.so" fwd_noenv ./tramp2 +# E103: the same run with a directory the host names FIRST holding a +# wrong-class libtgt.so. The walk used to stop at the first name that +# existed, dlopen then refused that file, and the shim reported no target +# even though the next named directory held one that loads. The shim skips +# a candidate of the other class and keeps walking. +confdir_decoy +run E103 OK "target /opt/cross-libc-unguessable-42/libtgt.so" fwd_noenv ./tramp2 # E75b: the control that must FAIL. Same directory, same library, same # binary, with the conf file gone, so nothing names it and the shim comes up # empty. Without this, E75 would also pass if the shim had simply @@ -1131,7 +1149,7 @@ run E75f OK "OK: next-provider ints=205" \ env LD_PRELOAD="$PWD/tgt-fwd.so" CROSS_LIBC_DLOPEN_GL_EAGER=1 ./nextprov # Left REMOVED, not restored: section P runs after this one and its aarch64 # shim would otherwise find the x86-64 libtgt.so through this very conf file. -rm -rf /opt/cross-libc-unguessable-42 +rm -rf /opt/cross-libc-unguessable-42 /opt/cross-libc-wrongclass-41 echo echo "-- P. the aarch64 trampolines, RUN -----------------------------" diff --git a/scripts/verify-gates.sh b/scripts/verify-gates.sh index e000f2a..68d8fad 100644 --- a/scripts/verify-gates.sh +++ b/scripts/verify-gates.sh @@ -93,7 +93,7 @@ g_attrib() { # section. Kept identical to the gates.yml step, including that. g_onehome() { f=0 - for n in 3470 358 65/65 61/61 45/45 40/40 26/26; do + for n in 3470 358 66/66 62/62 45/45 40/40 26/26; do c=$(git grep -lF "$n" -- '*.md' ':(exclude)docs/history/*' | sed 's|^docs/report/.*|docs/report/|' | sort -u | wc -l) [ "$c" -gt 1 ] && f=1 @@ -148,7 +148,7 @@ check "no tool is credited (tree)" g_attrib docs/_gate_probe.md \ check " the same, as a generated-with line" g_attrib docs/_gate_probe.md \ "Generated with [$TOOLNAME Code](https://example.invalid)" check "every headline number has one home" g_onehome docs/_gate_probe.md \ - 'the suite reports 65/65 predictions held' + 'the suite reports 66/66 predictions held' # ⛔ THIS CHECK EXISTS BECAUSE THE RATCHET DID NOT REFUSE. It was written as a # budget with a hardcoded number and a printed suggestion that the next reader diff --git a/src/gl-fwd.c b/src/gl-fwd.c index 85e9064..febb911 100644 --- a/src/gl-fwd.c +++ b/src/gl-fwd.c @@ -58,6 +58,8 @@ #endif #include #include +#include +#include #include #include #include @@ -868,12 +870,26 @@ struct glfwd_soname_lookup { char path[PATH_MAX]; }; +/* A candidate of the other ELF class cannot load, and the walk would stop on + * it before reaching the directory that can answer. Skip one, and leave a + * candidate that is not recognisable as an ELF to dlopen as before. */ +static int glfwd_wrong_class(const char *path) { + unsigned char id[EI_NIDENT]; + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return 0; + ssize_t n = read(fd, id, sizeof id); + close(fd); + return n == (ssize_t)sizeof id && memcmp(id, ELFMAG, SELFMAG) == 0 && + id[EI_CLASS] != (sizeof(void *) == 8 ? ELFCLASS64 : ELFCLASS32); +} + static int glfwd_try_soname(const char *dir, void *ctx) { struct glfwd_soname_lookup *c = ctx; char buf[PATH_MAX]; if (snprintf(buf, sizeof buf, "%s/%s", dir, c->name) >= (int)sizeof buf) return 0; - if (access(buf, R_OK) != 0) + if (access(buf, R_OK) != 0 || glfwd_wrong_class(buf)) return 0; snprintf(c->path, sizeof c->path, "%s", buf); return 1; From 1da40cd6f2d76f605726bd33c8cc84594dd817da Mon Sep 17 00:00:00 2001 From: Nemo <328747105+Nemo-010@users.noreply.github.com> Date: Thu, 24 Sep 2026 18:10:15 +0000 Subject: [PATCH 2/2] Record the release this fix owes once merged talaria0101's review of #41 notes that v0.2.6 was released from main before this branch, so no released artefact carries the wrong-class skip, and that the work order no longer says so. It does now. Refs #41. --- docs/todo/PROGRESS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/todo/PROGRESS.md b/docs/todo/PROGRESS.md index c2aab4c..7bf71bb 100644 --- a/docs/todo/PROGRESS.md +++ b/docs/todo/PROGRESS.md @@ -18,7 +18,9 @@ published. | `release` | ✅ on `v0.2.6` | **This branch fixes the host lookup that stopped on a library the loader would -refuse, on branch `fix/gl-fwd-wrong-class`.** +refuse, on branch `fix/gl-fwd-wrong-class`, and needs a release once merged. +`v0.2.6` was released from `main` before it, so no released artefact carries +the fix.** ---