From 4a784e56902391220225672067e4b9a7e6adae9f Mon Sep 17 00:00:00 2001 From: Patrick Date: Fri, 2 Oct 2026 13:05:45 +0200 Subject: [PATCH] Ask what a stub libprivileged-service-client.so must export (issue 105) The AU7200's build-3996334 report is the first trail on which the ladder's verdict is yes: every one of the five locations maps libovprobe.so executable and dlopen loads it, on one launch, in 243 ms, while the engine wall is otherwise the Q80's line for line. So the stub route the Q80 closed is open on this set, and what it needs next is the one thing the firmware will not let us read: the symbols the engine imports from the library it refuses. EngineImports asks the loader instead. It reads libchromium-impl.so'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 is what a stub must export, by name and kind. It runs on the post-failure probe thread ahead of the permission investigation, every step under a Deadline, and lands as an engine imports: header line, a block in the full report and trail lines. tools/engineimports/run.sh builds a library that needs a libblocked.so made unopenable by permission and holds the census to naming exactly its two symbols; the committed ARM libovprobe.so exercises the ELF32 path. --- CLAUDE.md | 19 + docs/INTERNALS.md | 76 ++++ src/common/EngineImports.cs | 586 +++++++++++++++++++++++++++++ src/common/SmackWall.cs | 7 + src/elm/BrowserApp.cs | 2 + tools/engineimports/Harness.csproj | 24 ++ tools/engineimports/Program.cs | 104 +++++ tools/engineimports/Stubs.cs | 35 ++ tools/engineimports/run.sh | 84 +++++ 9 files changed, 937 insertions(+) create mode 100644 src/common/EngineImports.cs create mode 100644 tools/engineimports/Harness.csproj create mode 100644 tools/engineimports/Program.cs create mode 100644 tools/engineimports/Stubs.cs create mode 100755 tools/engineimports/run.sh 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"