diff --git a/CLAUDE.md b/CLAUDE.md
index c824d7e..04e3754 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -297,6 +297,25 @@ from a TV that it did not, which is a reporter's evening each. The `hang` scenar
the one that holds it: against the previous `NativeProbe.cs` it does not fail, it
never returns.
+### The engine-imports harness
+
+Any change to `EngineImports` — the census of what a stub
+`libprivileged-service-client.so` would have to export (issue #105) — is exercised
+off-device first:
+
+```sh
+tools/engineimports/run.sh
+```
+
+It compiles the shipping `src/common/EngineImports.cs` against a trail stand-in,
+builds a library in this box's own architecture that needs `libm`, `libc` and a
+`libblocked.so` made unopenable by permission, and holds the census to naming
+exactly the blocked library's two symbols (a function and a data object), the
+refusal with the loader's words, nothing that another library provides, and no
+weak import. The committed ARM `libovprobe.so` exercises the ELF32 path, a FIFO
+holds it to a recorded miss rather than a hang, and a second call must change
+nothing. Needs the .NET 6 SDK under `~/.dotnet-local` and a C compiler.
+
### The trail harness
Any change to `Breadcrumbs` or `DiagLog` — the trail every diagnostic in this app
diff --git a/docs/INTERNALS.md b/docs/INTERNALS.md
index 958a7a7..f93e503 100644
--- a/docs/INTERNALS.md
+++ b/docs/INTERNALS.md
@@ -564,6 +564,10 @@ next one of these arrives:
the Q80's: refused in every location is the same wall, and closes it like #95; any
location that maps executable reopens the stub route, whose next question is which
symbols `libchromium-impl.so` imports from `libprivileged-service-client.so`.
+ **It answered on 2026-10-02, and the answer is the other one**: every location maps
+ executable and `dlopen` loads our library. The stub route is open on that set, and
+ the census that asks what the stub must export is the next build. See *A set that
+ passed the third gate* below; the state of #105 lives there.
- **The report names its build from `build-3996334` on.** Both reports open with
`build :`, the
release tag CI stamps into the assembly (`Directory.Build.props`,
@@ -1140,6 +1144,78 @@ None of that is a fourth question for the reporter. The question is still the
ladder's, and the wait is what gives it the time to answer in one launch; the markers
and the handler are there so the *next* silence names its side of the hand-over.
+### A set that passed the third gate: the AU7200 (issue #105)
+
+The `build-3996334` report from the AU7200 on Tizen 6.0 (2026-10-02) is the first
+trail on which the ladder's verdict is **yes**. On an install whose ledger already
+said the engine fails there, the walk went ahead of the engine from `OnCreate`,
+answered every rung of all five locations in 243 ms on one launch, and the engine
+waited for it as designed:
+
+```
+anonymous exec memory: ok
+res/ mmap PROT_READ|PROT_EXEC: ok dlopen: loaded, ov_probe_marker resolved
+bin/ mmap PROT_READ|PROT_EXEC: ok dlopen: loaded, ov_probe_marker resolved
+bin/ (assembly path) ... ok dlopen: loaded, ov_probe_marker resolved
+lib/ mmap PROT_READ|PROT_EXEC: ok dlopen: loaded, ov_probe_marker resolved
+data/ mmap PROT_READ|PROT_EXEC: ok dlopen: loaded, ov_probe_marker resolved
+native probe verdict: res/ maps executable and dlopen loaded it
+```
+
+The Smack labels are the expected `User::Pkg::org.apps2samsung.overscan::RO` on the
+read-only copies and the unsuffixed label on `data/`, all on the `vdfs` `/opt` mount.
+Then the engine wall, line for line the Q80's: nine EFL subsystems up, `ewk_init`
+returning 0 twice, `libchromium-impl.so` refused on `libprivileged-service-client.so:
+cannot open shared object file: Operation not permitted`, `RTLD_LAZY` refused the
+same way, the implementation file readable by us. So on this firmware the gate that
+refuses the engine's helper library does **not** refuse a native library of ours. Of
+the three gates the stub route was given in *What is left on the Q80*, this is the one
+the Q80 failed and the AU7200 passes. The reporter also says the set has not had a
+firmware update in a long time, which is consistent with the gate being older
+firmware's rather than newer.
+
+What the stub route needs next is the one thing the firmware will not let us read:
+the symbols the engine imports from the refused library, which is the file it will
+not open. `EngineImports` (`src/common`) asks the loader instead. It reads the
+implementation's own dynamic section — DT_NEEDED and every undefined dynamic symbol,
+ELF32 on the set and ELF64 for the harness — dlopens each DT_NEEDED library by soname
+and then by path in the engine's directory, recording each refusal with the loader's
+words, and looks every non-weak import up in the process and in each library that
+loaded. What is left unresolved is what a stub must export, by name and kind
+(`FUNC`, `OBJECT`). It runs on the post-failure probe thread, after `NativeProbe.Run`
+and **ahead of** `SmackWall.Investigate`, because the investigation's last step is
+the one the Q80 never came back from and this census is the question the build is
+for; every step past managed code is under a `Deadline`. The report has an
+`engine imports:` header line and a *what a stub must provide* block; the trail has
+`engine imports:` lines with up to forty names. `tools/engineimports/run.sh` builds a
+library in this box's architecture that needs a `libblocked.so` made unopenable by
+permission, and holds the census to naming exactly that library's two symbols, one
+function and one data object, with `cos` and `strlen` resolved elsewhere and the weak
+imports set aside; the committed ARM `libovprobe.so` exercises the ELF32 path.
+
+How to read the next report, said here before it exists so neither reading is a
+reversal later:
+
+- `engine imports: N needed (1 refused: libprivileged-service-client.so), M imports,
+ U unresolved` with a short list of plain C names (`FUNC`) — a no-op stub is
+ buildable with the `tools/elfprobe` recipe, exporting those names, and the build
+ after it ships the stub in `lib/` and dlopens it `RTLD_GLOBAL` by absolute path
+ before the implementation. That build is the one that says whether the AU7200
+ runs Overscan.
+- Names beginning `_Z` (C++) or `OBJECT`s among them — the engine reads data or
+ calls methods out of that library, and a stub has to carry real behaviour. Worth
+ one look at what the names say before deciding whether to try.
+- More than one library refused — the stub has to stand in for each, and the odds
+ fall with every one.
+- `DID NOT RETURN` on the read or the resolve, or an `engine imports:` line that
+ never arrives — the set parks the census like the Q80 parked its calls; the
+ previous-run block says which step.
+
+**State of #105 (2026-10-02):** the ladder's answer is in and is the good one; the
+census build is the next thing the reporter is asked to run. A reply quoting its tag
+is drafted and goes out when Patrick says so. Nothing here changes the Q80's close:
+that set refused the mapping the AU7200 allows.
+
### `ELM_ACCEL` has to be set before the window exists
`libchromium-ewk.so` has a library constructor whose entire body is
diff --git a/src/common/EngineImports.cs b/src/common/EngineImports.cs
new file mode 100644
index 0000000..29aedbc
--- /dev/null
+++ b/src/common/EngineImports.cs
@@ -0,0 +1,586 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Overscan
+{
+ ///
+ /// Asks what a stub libprivileged-service-client.so would have to provide:
+ /// the symbols the engine's implementation imports that no library this process
+ /// may load provides.
+ ///
+ /// Issue #105, an AU7200 on Tizen 6.0, is the first set to pass the third gate of
+ /// the stub route (see *What is left on the Q80* in docs/INTERNALS.md). The Q80
+ /// refused to map any file we ship executable, so a stub had nowhere to live and
+ /// #17 was closed on it; the AU7200's `build-3996334` report maps `libovprobe.so`
+ /// executable and dlopens it in all five locations, on one launch, in 243 ms. The
+ /// engine wall on that set is otherwise the Q80's line for line:
+ /// `libchromium-impl.so` refused on `libprivileged-service-client.so: Operation
+ /// not permitted`, with the implementation file itself readable by us.
+ ///
+ /// So the route is: ship a stub under that soname, dlopen it by absolute path
+ /// with RTLD_GLOBAL before the implementation, and the loader satisfies the
+ /// implementation's DT_NEEDED from the soname already in the process instead of
+ /// going to /usr/lib for it. What the stub must export is the one thing we cannot
+ /// read off the firmware, because the real library is the file it will not let us
+ /// open. But the implementation is readable, and the loader can answer the rest:
+ ///
+ /// 1. Read the implementation's dynamic section: its DT_NEEDED list and every
+ /// undefined dynamic symbol (ELF32 on the set; ELF64 too, for the harness).
+ /// 2. dlopen every DT_NEEDED library, recording each one that is refused and why.
+ /// 3. Look every undefined symbol up in the process and in each library that did
+ /// load. Whatever is still unresolved is what the stub has to provide, by name.
+ ///
+ /// Each step past managed code goes out under a , because
+ /// a call this firmware objects to is parked rather than refused, and it runs on
+ /// the post-failure probe thread after the engine has already failed: whatever
+ /// is most likely not to survive the firmware goes last. It is placed ahead of
+ /// the permission investigation on that thread because the investigation's last
+ /// step is the one the Q80 never came back from, and this census is the question
+ /// the next build is waiting on.
+ ///
+ /// How to read it: a short list of plain C function names is a no-op stub,
+ /// buildable with the same toolchain recipe as `libovprobe.so`. C++ names (`_Z…`)
+ /// or data objects are a stub that has to do real work. A second refused library
+ /// means the stub has to stand in for two. Weak undefined symbols are counted and
+ /// set aside: the loader does not need them.
+ ///
+ internal static class EngineImports
+ {
+ private const int RtldLazy = 0x0001;
+ private const int RtldLocal = 0x0000;
+
+ /// How many unresolved names the trail carries; the dump has them all.
+ private const int TrailNames = 40;
+
+ [DllImport("libdl.so.2")]
+ private static extern IntPtr dlopen(string file, int mode);
+
+ [DllImport("libdl.so.2")]
+ private static extern IntPtr dlerror();
+
+ [DllImport("libdl.so.2")]
+ private static extern IntPtr dlsym(IntPtr handle, string symbol);
+
+ private static readonly string[] Candidates =
+ {
+ "/usr/share/chromium-efl/lib/libchromium-impl.so",
+ "/usr/lib/libchromium-impl.so",
+ "/usr/share/chromium-efl/libchromium-impl.so",
+ };
+
+ private static readonly object Gate = new object();
+ private static readonly List Lines = new List();
+ private static bool _ran;
+
+ /// One line for the diagnostics header.
+ public static string Summary { get; private set; } = "(not asked)";
+
+ /// The DT_NEEDED sonames the loader refused, with the loader's words.
+ public static IList Refused { get; private set; } = new List();
+
+ /// The imports no loadable library provides — what a stub must export.
+ public static IList Unresolved { get; private set; } = new List();
+
+ /// The whole census, for the full report.
+ public static string Dump()
+ {
+ lock (Gate)
+ {
+ return Lines.Count == 0 ? "(not asked)\n" : string.Join("\n", Lines.ToArray()) + "\n";
+ }
+ }
+
+ private static void Note(string line)
+ {
+ lock (Gate)
+ {
+ Lines.Add(line);
+ }
+ }
+
+ private static void Trace(string line)
+ {
+ Note(line);
+ Breadcrumbs.Drop("engine imports: " + line);
+ }
+
+ ///
+ /// Runs the census once. is the path
+ /// ChromiumImpl found, or null to look in the known places;
+ /// is the soname the loader refused, for the summary.
+ /// Never throws: a diagnostic has nobody to throw to.
+ ///
+ public static void Census(string implementation, string blocked)
+ {
+ lock (Gate)
+ {
+ if (_ran)
+ {
+ return;
+ }
+
+ _ran = true;
+ }
+
+ try
+ {
+ Run(implementation, blocked);
+ }
+ catch (Exception ex)
+ {
+ Summary = "threw " + ex.GetType().Name + ": " + ex.Message;
+ Trace(Summary);
+ }
+ }
+
+ private static void Run(string implementation, string blocked)
+ {
+ string path = implementation;
+ if (path == null)
+ {
+ foreach (string candidate in Candidates)
+ {
+ if (File.Exists(candidate))
+ {
+ path = candidate;
+ break;
+ }
+ }
+ }
+
+ if (path == null)
+ {
+ Summary = "libchromium-impl.so is not at any known path";
+ Trace(Summary);
+ return;
+ }
+
+ Summary = "reading " + path;
+ Trace("reading " + path);
+
+ Elf elf = null;
+ string parsed = Deadline.Run(delegate { elf = Parse(path); return "ok"; }, 15000);
+ if (parsed != "ok" || elf == null)
+ {
+ Summary = "reading " + path + ": " + parsed;
+ Trace(Summary);
+ return;
+ }
+
+ int weak = 0;
+ foreach (Import import in elf.Imports)
+ {
+ if (import.Weak)
+ {
+ weak++;
+ }
+ }
+
+ Trace("ELF" + elf.Class + (elf.Soname == null ? string.Empty : " " + elf.Soname) + ": " +
+ elf.Needed.Count + " needed, " + elf.Imports.Count + " imports (" + weak + " weak)");
+ foreach (string soname in elf.Needed)
+ {
+ Note(" needed: " + soname);
+ }
+
+ // Step 2: every DT_NEEDED, by soname first (the loader's own search) and
+ // then by path in the engine's directories, which the loader does not
+ // search for us. One refusal line each; the ones that load are not news.
+ var handles = new List>();
+ var refused = new List();
+ string directory = Path.GetDirectoryName(path) ?? "/usr/lib";
+ foreach (string soname in elf.Needed)
+ {
+ IntPtr handle = IntPtr.Zero;
+ string error = null;
+ string answer = Deadline.Run(delegate
+ {
+ handle = Open(soname, out error);
+ if (handle == IntPtr.Zero && IsNotFound(error))
+ {
+ string here = Path.Combine(directory, soname);
+ if (File.Exists(here))
+ {
+ handle = Open(here, out error);
+ }
+ }
+
+ return handle == IntPtr.Zero ? "refused" : "loaded";
+ });
+
+ if (answer == "loaded")
+ {
+ handles.Add(new KeyValuePair(soname, handle));
+ Note(" loaded: " + soname);
+ continue;
+ }
+
+ string why = answer == Deadline.Missed ? Deadline.Missed : (error ?? answer);
+ refused.Add(soname + " — " + why);
+ Trace("refused " + soname + " — " + why);
+ }
+
+ // Step 3: the process first (libc and everything already in it), then
+ // each library that loaded. Weak imports are the loader's business, not
+ // a stub's; they are set aside rather than asked.
+ var unresolved = new List();
+ var providers = new Dictionary();
+ string resolved = Deadline.Run(delegate
+ {
+ foreach (Import import in elf.Imports)
+ {
+ if (import.Weak)
+ {
+ continue;
+ }
+
+ string provider = null;
+ if (dlsym(IntPtr.Zero, import.Name) != IntPtr.Zero)
+ {
+ provider = "(process)";
+ }
+ else
+ {
+ foreach (KeyValuePair pair in handles)
+ {
+ if (dlsym(pair.Value, import.Name) != IntPtr.Zero)
+ {
+ provider = pair.Key;
+ break;
+ }
+ }
+ }
+
+ if (provider == null)
+ {
+ unresolved.Add(import.Name + " (" + import.Kind + ")");
+ }
+ else
+ {
+ int n;
+ providers.TryGetValue(provider, out n);
+ providers[provider] = n + 1;
+ }
+ }
+
+ return "ok";
+ }, 30000);
+
+ if (resolved != "ok")
+ {
+ Summary = "resolving imports: " + resolved;
+ Trace(Summary);
+ return;
+ }
+
+ foreach (KeyValuePair pair in providers)
+ {
+ Note(" provides " + pair.Value.ToString(CultureInfo.InvariantCulture) + ": " + pair.Key);
+ }
+
+ int cpp = 0;
+ foreach (string name in unresolved)
+ {
+ if (name.StartsWith("_Z", StringComparison.Ordinal))
+ {
+ cpp++;
+ }
+ }
+
+ Trace(unresolved.Count + " unresolved" + (cpp > 0 ? " (" + cpp + " C++)" : string.Empty) +
+ (unresolved.Count > 0 ? " — what a stub must provide" : " — nothing for a stub to provide"));
+ for (int i = 0; i < unresolved.Count; i++)
+ {
+ if (i < TrailNames)
+ {
+ Trace(" import: " + unresolved[i]);
+ }
+ else
+ {
+ Note(" import: " + unresolved[i]);
+ }
+ }
+
+ if (unresolved.Count > TrailNames)
+ {
+ Trace(" … " + (unresolved.Count - TrailNames) + " more in the full report");
+ }
+
+ Refused = refused;
+ Unresolved = unresolved;
+
+ var summary = new StringBuilder();
+ summary.Append(elf.Needed.Count).Append(" needed");
+ if (refused.Count > 0)
+ {
+ summary.Append(" (").Append(refused.Count).Append(" refused: ");
+ for (int i = 0; i < refused.Count; i++)
+ {
+ if (i > 0)
+ {
+ summary.Append(", ");
+ }
+
+ string soname = refused[i];
+ int dash = soname.IndexOf(" — ", StringComparison.Ordinal);
+ summary.Append(dash > 0 ? soname.Substring(0, dash) : soname);
+ }
+
+ summary.Append(")");
+ }
+
+ summary.Append(", ").Append(elf.Imports.Count).Append(" imports, ")
+ .Append(unresolved.Count).Append(" unresolved");
+ if (unresolved.Count > 0 && unresolved.Count <= 6)
+ {
+ summary.Append(": ").Append(string.Join(", ", unresolved.ToArray()));
+ }
+
+ if (blocked != null && refused.Count == 0)
+ {
+ summary.Append(" — ").Append(blocked).Append(" loaded this time");
+ }
+
+ Summary = summary.ToString();
+ Trace("done — " + Summary);
+ }
+
+ private static IntPtr Open(string file, out string error)
+ {
+ error = null;
+ dlerror();
+ IntPtr handle = dlopen(file, RtldLazy | RtldLocal);
+ if (handle == IntPtr.Zero)
+ {
+ IntPtr message = dlerror();
+ error = message == IntPtr.Zero ? "dlopen failed without a message" : Marshal.PtrToStringAnsi(message);
+ }
+
+ return handle;
+ }
+
+ private static bool IsNotFound(string error)
+ {
+ return error != null && error.IndexOf("No such file", StringComparison.Ordinal) >= 0;
+ }
+
+ private sealed class Import
+ {
+ public string Name;
+ public string Kind;
+ public bool Weak;
+ }
+
+ private sealed class Elf
+ {
+ public int Class;
+ public string Soname;
+ public readonly List Needed = new List();
+ public readonly List Imports = new List();
+ }
+
+ ///
+ /// The dynamic section and the dynamic symbol table, off the section headers.
+ /// Little-endian only, which both the set and the harness are. Throws on a
+ /// file that is not what it claims; the caller turns that into a line.
+ ///
+ private static Elf Parse(string path)
+ {
+ using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
+ using (var reader = new BinaryReader(stream))
+ {
+ byte[] ident = reader.ReadBytes(16);
+ if (ident.Length < 16 || ident[0] != 0x7F || ident[1] != (byte)'E' || ident[2] != (byte)'L' || ident[3] != (byte)'F')
+ {
+ throw new InvalidDataException("not an ELF");
+ }
+
+ if (ident[5] != 1)
+ {
+ throw new InvalidDataException("big-endian ELF, not handled");
+ }
+
+ var elf = new Elf();
+ elf.Class = ident[4] == 2 ? 64 : 32;
+ bool wide = elf.Class == 64;
+
+ reader.ReadUInt16(); // e_type
+ reader.ReadUInt16(); // e_machine
+ reader.ReadUInt32(); // e_version
+ Word(reader, wide); // e_entry
+ Word(reader, wide); // e_phoff
+ long shoff = (long)Word(reader, wide);
+ reader.ReadUInt32(); // e_flags
+ reader.ReadUInt16(); // e_ehsize
+ reader.ReadUInt16(); // e_phentsize
+ reader.ReadUInt16(); // e_phnum
+ int shentsize = reader.ReadUInt16();
+ int shnum = reader.ReadUInt16();
+ reader.ReadUInt16(); // e_shstrndx
+
+ if (shoff <= 0 || shnum <= 0)
+ {
+ throw new InvalidDataException("no section headers");
+ }
+
+ // Section headers: type, link, offset, size and entsize are what the
+ // two tables need; the others are read past.
+ var sections = new List();
+ for (int i = 0; i < shnum; i++)
+ {
+ stream.Seek(shoff + (long)i * shentsize, SeekOrigin.Begin);
+ var section = new Section();
+ reader.ReadUInt32(); // sh_name
+ section.Type = reader.ReadUInt32(); // sh_type
+ Word(reader, wide); // sh_flags
+ Word(reader, wide); // sh_addr
+ section.Offset = (long)Word(reader, wide); // sh_offset
+ section.Size = (long)Word(reader, wide); // sh_size
+ section.Link = (int)reader.ReadUInt32(); // sh_link
+ reader.ReadUInt32(); // sh_info
+ Word(reader, wide); // sh_addralign
+ section.EntSize = (long)Word(reader, wide);
+ sections.Add(section);
+ }
+
+ Section dynamic = null, dynsym = null;
+ foreach (Section section in sections)
+ {
+ if (section.Type == 6 && dynamic == null) { dynamic = section; }
+ if (section.Type == 11 && dynsym == null) { dynsym = section; }
+ }
+
+ if (dynamic == null || dynsym == null)
+ {
+ throw new InvalidDataException("no dynamic section or symbol table");
+ }
+
+ // DT_NEEDED and DT_SONAME, names out of the string table the dynamic
+ // section links to.
+ Section dynstr = Table(sections, dynamic.Link);
+ long count = dynamic.EntSize > 0 ? dynamic.Size / dynamic.EntSize : 0;
+ var soname = new List();
+ var needed = new List();
+ stream.Seek(dynamic.Offset, SeekOrigin.Begin);
+ for (long i = 0; i < count; i++)
+ {
+ long tag = (long)Word(reader, wide);
+ ulong value = Word(reader, wide);
+ if (tag == 0) { break; }
+ if (tag == 1) { needed.Add((long)value); }
+ if (tag == 14) { soname.Add((long)value); }
+ }
+
+ foreach (long offset in needed)
+ {
+ elf.Needed.Add(Name(stream, reader, dynstr, offset));
+ }
+
+ if (soname.Count > 0)
+ {
+ elf.Soname = Name(stream, reader, dynstr, soname[0]);
+ }
+
+ // Undefined dynamic symbols: st_shndx == SHN_UNDEF with a name.
+ Section symstr = Table(sections, dynsym.Link);
+ long symbols = dynsym.EntSize > 0 ? dynsym.Size / dynsym.EntSize : 0;
+ var raw = new List();
+ var names = new List();
+ for (long i = 1; i < symbols; i++)
+ {
+ stream.Seek(dynsym.Offset + i * dynsym.EntSize, SeekOrigin.Begin);
+ long name;
+ byte info;
+ int shndx;
+ if (wide)
+ {
+ name = reader.ReadUInt32();
+ info = reader.ReadByte();
+ reader.ReadByte(); // st_other
+ shndx = reader.ReadUInt16();
+ }
+ else
+ {
+ name = reader.ReadUInt32();
+ reader.ReadUInt32(); // st_value
+ reader.ReadUInt32(); // st_size
+ info = reader.ReadByte();
+ reader.ReadByte(); // st_other
+ shndx = reader.ReadUInt16();
+ }
+
+ if (shndx != 0 || name == 0)
+ {
+ continue;
+ }
+
+ int bind = info >> 4, type = info & 0xF;
+ var import = new Import();
+ import.Weak = bind == 2;
+ import.Kind = type == 1 ? "OBJECT" : type == 2 ? "FUNC" : type == 10 ? "IFUNC" : type == 6 ? "TLS" : "type " + type;
+ raw.Add(import);
+ names.Add(name);
+ }
+
+ for (int i = 0; i < raw.Count; i++)
+ {
+ raw[i].Name = Name(stream, reader, symstr, names[i]);
+ if (raw[i].Name.Length > 0)
+ {
+ elf.Imports.Add(raw[i]);
+ }
+ }
+
+ return elf;
+ }
+ }
+
+ private sealed class Section
+ {
+ public uint Type;
+ public long Offset;
+ public long Size;
+ public int Link;
+ public long EntSize;
+ }
+
+ private static Section Table(List sections, int index)
+ {
+ if (index < 0 || index >= sections.Count || sections[index].Type != 3)
+ {
+ throw new InvalidDataException("string table " + index + " is not one");
+ }
+
+ return sections[index];
+ }
+
+ private static ulong Word(BinaryReader reader, bool wide)
+ {
+ return wide ? reader.ReadUInt64() : reader.ReadUInt32();
+ }
+
+ private static string Name(Stream stream, BinaryReader reader, Section table, long offset)
+ {
+ if (offset < 0 || offset >= table.Size)
+ {
+ return string.Empty;
+ }
+
+ stream.Seek(table.Offset + offset, SeekOrigin.Begin);
+ var bytes = new List();
+ long limit = Math.Min(table.Size - offset, 4096);
+ for (long i = 0; i < limit; i++)
+ {
+ byte b = reader.ReadByte();
+ if (b == 0) { break; }
+ bytes.Add(b);
+ }
+
+ return Encoding.ASCII.GetString(bytes.ToArray());
+ }
+ }
+}
diff --git a/src/common/SmackWall.cs b/src/common/SmackWall.cs
index ef6c528..0326cc4 100644
--- a/src/common/SmackWall.cs
+++ b/src/common/SmackWall.cs
@@ -177,6 +177,12 @@ public static void InvestigateInBackground(string target)
// this Run finds it taken and says so; see
// NativeProbe.StartEarlyIfUnfinished.
NativeProbe.Run();
+
+ // Issue #105: what a stub would have to export, ahead of the
+ // investigation because the investigation's last step is the
+ // one the Q80 never came back from, and this is the question
+ // the next build on a set that passed the ladder waits on.
+ EngineImports.Census(ChromiumImpl.Implementation, target);
Investigate(target);
});
@@ -192,6 +198,7 @@ public static void InvestigateInBackground(string target)
// just gets it the dangerous way.
Trace("probe: no thread (" + ex.GetType().Name + "), running inline");
NativeProbe.Run();
+ EngineImports.Census(ChromiumImpl.Implementation, target);
Investigate(target);
}
}
diff --git a/src/elm/BrowserApp.cs b/src/elm/BrowserApp.cs
index 51a1dae..0a10951 100644
--- a/src/elm/BrowserApp.cs
+++ b/src/elm/BrowserApp.cs
@@ -2337,11 +2337,13 @@ private string DiagnosticsText(bool full)
? "(nothing was refused)"
: ChromiumImpl.Blocked + " — " + SmackWall.Summary) + "\n" +
"own native : " + NativeProbe.Summary + "\n" +
+ "engine imports: " + EngineImports.Summary + "\n" +
"trail file : " + Breadcrumbs.Location + "\n" +
"trail write: " + Breadcrumbs.Status + "\n" +
(full ? "\nefl ladder (ewk_init's own order)\n" + EflSubsystems.Dump() +
"\nengine implementation (libchromium-impl.so)\n" + ChromiumImpl.Dump() +
"\nnative code of our own\n" + NativeProbe.Dump() +
+ "\nwhat a stub must provide (the implementation's imports)\n" + EngineImports.Dump() +
"\npermission wall" +
(ChromiumImpl.Blocked == null
? "\n"
diff --git a/tools/engineimports/Harness.csproj b/tools/engineimports/Harness.csproj
new file mode 100644
index 0000000..2b8ad6a
--- /dev/null
+++ b/tools/engineimports/Harness.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+ Exe
+ net6.0
+ engineimports
+ Overscan.Harness
+ false
+ true
+ 0.1.0
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/engineimports/Program.cs b/tools/engineimports/Program.cs
new file mode 100644
index 0000000..2d7f470
--- /dev/null
+++ b/tools/engineimports/Program.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+
+namespace Overscan.Harness
+{
+ ///
+ /// Drives EngineImports.Census over the libraries run.sh built, one scenario per
+ /// process because the census's books are static.
+ ///
+ /// census the real shape: one dependency refused
+ /// clean nothing refused, nothing unresolved
+ /// hang a file that never answers
+ /// missing no implementation at all
+ ///
+ internal static class Program
+ {
+ private static readonly List Fails = new List();
+
+ private static void Expect(bool ok, string what)
+ {
+ if (!ok)
+ {
+ Fails.Add(what);
+ }
+ }
+
+ private static int Main(string[] args)
+ {
+ string scenario = args.Length > 0 ? args[0] : "";
+ string path = args.Length > 1 ? args[1] : null;
+ string blocked = args.Length > 2 ? args[2] : null;
+
+ var started = DateTime.UtcNow;
+ EngineImports.Census(path, blocked);
+ double seconds = (DateTime.UtcNow - started).TotalSeconds;
+
+ string summary = EngineImports.Summary;
+ string dump = EngineImports.Dump();
+ string trail = Breadcrumbs.Trail;
+ Console.WriteLine(" summary: " + summary);
+
+ switch (scenario)
+ {
+ case "census":
+ Expect(trail.Contains("engine imports: reading " + path), "the trail names the file read");
+ Expect(trail.Contains(" needed, ") && trail.Contains(" imports ("), "the trail has the counts line");
+ Expect(trail.Contains("engine imports: refused " + blocked + " — "), "the refused dependency is named with the loader's words");
+ Expect(!trail.Contains("refused libm.so.6") && !trail.Contains("refused libc.so.6"), "libraries that load are not reported refused");
+ Expect(EngineImports.Refused.Count == 1, "exactly one refusal, got " + EngineImports.Refused.Count);
+ Expect(EngineImports.Unresolved.Count == 2, "exactly the two symbols of the blocked library, got " + EngineImports.Unresolved.Count + ": " + string.Join(", ", EngineImports.Unresolved));
+ Expect(trail.Contains(" import: blocked_alpha (FUNC)"), "blocked_alpha named as a function");
+ Expect(trail.Contains(" import: blocked_counter (OBJECT)"), "blocked_counter named as a data object");
+ Expect(!trail.Contains("import: cos") && !trail.Contains("import: strlen"), "symbols another library provides are not listed");
+ Expect(!trail.Contains("import: __gmon_start__") && !trail.Contains("import: __cxa_finalize"), "weak imports are set aside");
+ Expect(dump.Contains(" needed: " + blocked) && dump.Contains(" needed: libm.so.6"), "the dump lists every DT_NEEDED");
+ Expect(dump.Contains(" loaded: libm.so.6"), "the dump says which dependencies loaded");
+ Expect(dump.Contains("provides") && (dump.Contains("libm.so.6") || dump.Contains("(process)")), "the dump says who provides what resolved");
+ Expect(summary.Contains("(1 refused: " + blocked + ")"), "the summary names the refusal");
+ Expect(summary.Contains("2 unresolved: blocked_alpha (FUNC), blocked_counter (OBJECT)"), "a short unresolved list is on the summary line");
+ Expect(trail.Contains("engine imports: done — "), "the trail ends on done");
+ break;
+
+ case "clean":
+ Expect(EngineImports.Refused.Count == 0, "nothing refused");
+ Expect(EngineImports.Unresolved.Count == 0, "nothing unresolved, got: " + string.Join(", ", EngineImports.Unresolved));
+ Expect(summary.Contains("0 unresolved"), "summary says 0 unresolved");
+ Expect(trail.Contains("ELF32") || trail.Contains("ELF64"), "the ELF class is named");
+ break;
+
+ case "hang":
+ Expect(summary.Contains(Deadline.Missed), "a file that never answers is a recorded miss, got: " + summary);
+ Expect(seconds < 60, "the miss came back under the deadline (" + seconds.ToString("0.0") + "s)");
+ Expect(!trail.Contains("done —"), "no done line on a miss");
+ break;
+
+ case "missing":
+ Expect(summary.Contains("not at any known path") || summary.Contains("reading"), "a missing implementation is said so, got: " + summary);
+ Expect(!trail.Contains("done —"), "no done line without a file");
+ break;
+
+ default:
+ Fails.Add("unknown scenario " + scenario);
+ break;
+ }
+
+ // Whatever the scenario, a second call must be a no-op.
+ EngineImports.Census(path, blocked);
+ Expect(EngineImports.Summary == summary, "a second Census changed the summary");
+
+ if (Fails.Count == 0)
+ {
+ Console.WriteLine(" ok");
+ return 0;
+ }
+
+ foreach (string fail in Fails)
+ {
+ Console.WriteLine(" FAIL " + fail);
+ }
+
+ return 1;
+ }
+ }
+}
diff --git a/tools/engineimports/Stubs.cs b/tools/engineimports/Stubs.cs
new file mode 100644
index 0000000..9aa04ad
--- /dev/null
+++ b/tools/engineimports/Stubs.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+
+// The one surface EngineImports reaches for besides the loader: the trail. Kept in
+// memory here and read back by the assertions.
+namespace Overscan
+{
+ internal static class Breadcrumbs
+ {
+ private static readonly List Written = new List();
+
+ private static readonly object Gate = new object();
+
+ public static void Drop(string message)
+ {
+ lock (Gate)
+ {
+ Written.Add(message);
+ }
+
+ Console.WriteLine(" | " + message);
+ }
+
+ public static string Trail
+ {
+ get
+ {
+ lock (Gate)
+ {
+ return string.Join("\n", Written.ToArray());
+ }
+ }
+ }
+ }
+}
diff --git a/tools/engineimports/run.sh b/tools/engineimports/run.sh
new file mode 100755
index 0000000..302b931
--- /dev/null
+++ b/tools/engineimports/run.sh
@@ -0,0 +1,84 @@
+#!/usr/bin/env bash
+# Exercises EngineImports — the census of what a stub libprivileged-service-client.so
+# would have to export (issue #105) — off-device, against the shipping file.
+#
+# ./run.sh
+#
+# The census reads the engine implementation's dynamic section, dlopens each of its
+# DT_NEEDED libraries, and names the imports that nothing loadable provides. Every
+# way it can be wrong costs a reporter an evening: a symbol wrongly listed puts a
+# useless export in the stub, a symbol wrongly omitted makes the stub fail to load
+# the engine, and a parse that is off by a field names nonsense with confidence.
+#
+# So the harness builds the real shape in this box's own architecture: a library
+# that needs libm, libc and a `libblocked.so`, imports two symbols from the latter
+# (one function, one data object), and then makes libblocked.so unopenable the way
+# the set does (a permission refusal, not a missing file). The census has to name
+# exactly those two, say libblocked.so was refused and why, and leave cos and
+# strlen out of it. The ELF32 path is exercised on the committed ARM
+# Overscan5/res/libovprobe.so, which has nothing to import and must say so.
+set -euo pipefail
+cd "$(dirname "$0")"
+
+DOTNET="${DOTNET:-$HOME/.dotnet-local/dotnet}"
+export DOTNET_ROOT="${DOTNET_ROOT:-$HOME/.dotnet-local}"
+export DOTNET_CLI_TELEMETRY_OPTOUT=1
+export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
+
+if [ ! -x "$DOTNET" ]; then
+ echo "no dotnet at $DOTNET — see the toolchain notes in ../../build.sh" >&2
+ exit 2
+fi
+
+CC="${CC:-cc}"
+if ! command -v "$CC" >/dev/null; then
+ echo "no C compiler ($CC) to build the stand-in libraries — install one rather than skipping this" >&2
+ exit 2
+fi
+
+work="$(mktemp -d)"
+trap 'chmod -R u+w "$work" 2>/dev/null || true; rm -rf "$work"' EXIT
+
+echo "== building the harness (with src/common/EngineImports.cs itself)"
+"$DOTNET" build Harness.csproj -c Release -o "$work/bin" --nologo -v quiet
+census="$work/bin/engineimports"
+
+echo "== building the stand-in libraries"
+mkdir -p "$work/lib"
+printf 'int blocked_counter = 7;\nint blocked_alpha(int x) { return x + blocked_counter; }\n' > "$work/blocked.c"
+"$CC" -shared -fPIC -Wl,-soname,libblocked.so -o "$work/lib/libblocked.so" "$work/blocked.c"
+cat > "$work/needy.c" <<'C'
+#include
+#include
+extern int blocked_counter;
+int blocked_alpha(int x);
+double needy_use(const char *s) { return cos((double)strlen(s)) + blocked_alpha(blocked_counter); }
+C
+"$CC" -shared -fPIC -Wl,-soname,libneedy.so -Wl,--no-as-needed -L"$work/lib" -o "$work/lib/libneedy.so" "$work/needy.c" -lblocked -lm
+
+run() {
+ local scenario="$1"; shift
+ echo "== $scenario"
+ timeout 120 "$census" "$scenario" "$@"
+}
+
+# The real shape. libblocked.so stays on disk, next to the implementation the way
+# the set's is, and is made unopenable: the loader says "Permission denied", the
+# set says "Operation not permitted", and the census only has to carry the words.
+chmod 000 "$work/lib/libblocked.so"
+run census "$work/lib/libneedy.so" libblocked.so
+chmod 644 "$work/lib/libblocked.so"
+
+# Nothing to import at all, on the committed ARM ELF32.
+run clean ../../Overscan5/res/libovprobe.so
+
+# A file that never answers: a FIFO with no writer blocks the open for as long as
+# the census is willing to wait.
+mkfifo "$work/hang.so"
+run hang "$work/hang.so"
+
+# No implementation anywhere.
+run missing "$work/does-not-exist.so"
+
+echo
+echo "engineimports: all checks passed"