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"