diff --git a/docs/CHALLENGES.md b/docs/CHALLENGES.md index 72af4b3..51d9bfd 100644 --- a/docs/CHALLENGES.md +++ b/docs/CHALLENGES.md @@ -7,7 +7,7 @@ the source. The pattern across them is the reason the document exists: **almost nothing here was found by reading code or by a test written from imagination.** Of -407 defects, 327 were found only by running against a real device. Eighteen +408 defects, 328 were found only by running against a real device. Eighteen more — 255, 280, 282, 327, 329, 353, 355, 356, 359, 371, 375, 380, 382, 384, 386, 387, 388 and 406 — were met while running against one, though the fault lay in a check, the host or Mobium's own design rather than in the platform. The other sixty-two — 4, 5, 6, 14, 23, @@ -8885,6 +8885,20 @@ bytecode. It prints where and what kind, never the value, since a public repository's CI log is public. Its selftest holds each rule to a case it must flag, and a leak planted in a staged file failed `make docs-check`. +### 408. A car's screenshots carried a warning after the image + +**Found by:** stills for Morpho Market's README, cut from a trace of checkout +test A on the Android Automotive 15 emulator. `ffmpeg` read every one as +damaged. 38 of the 39 screenshots, and a plain `mobium screenshot`, ended in +"FORTIFY: pthread_mutex_lock called on a destroyed mutex", which `screencap` +wrote to stdout after the PNG's IEND chunk; image viewers ignore it, strict +decoders do not. The same capture opens with "[Warning] Multiple displays +were found", which 211's rule already skips. + +**Fixed:** a capture is cut at its IEND chunk when a short line of text +follows, the rule 211 applies to text before it; anything else after it is +refused. The same screenshot then ended at its IEND. + ## Findings that were not defects Worth recording because each one closed off an approach that looked obvious. diff --git a/docs/RELEASE-CHECKLIST.md b/docs/RELEASE-CHECKLIST.md index 750cf22..dc5c2cb 100644 --- a/docs/RELEASE-CHECKLIST.md +++ b/docs/RELEASE-CHECKLIST.md @@ -4,7 +4,7 @@ [CI workflow](../.github/workflows/ci.yml) runs on every push — formatting, vet, both modules' tests, the four non-Go clients' own tests, and six cross-compile targets. This file is the rest, and the rest is where most -serious defects in this project have come from: 327 of 407 were found only by +serious defects in this project have come from: 328 of 408 were found only by running against a real device. Work through it before tagging a release, on three substrates: an Android diff --git a/internal/mobiumdriver/android.go b/internal/mobiumdriver/android.go index 36c4843..c53ee19 100644 --- a/internal/mobiumdriver/android.go +++ b/internal/mobiumdriver/android.go @@ -221,7 +221,8 @@ func (a *Android) Screenshot(ctx context.Context) ([]byte, error) { return png, nil } -// maxVendorPrefix bounds the text skipped ahead of a capture's PNG. +// maxVendorPrefix bounds the text skipped ahead of a capture's PNG, and cut +// after it. const maxVendorPrefix = 4096 // pngIn is the PNG in what screencap wrote, past any lines a vendor library @@ -236,14 +237,36 @@ func pngIn(out []byte) ([]byte, bool) { if i < 0 { return nil, false } - if i == 0 { - return out, true + if i > 0 { + prefix := out[:i] + if i > maxVendorPrefix || prefix[len(prefix)-1] != '\n' || bytes.IndexByte(prefix, 0) >= 0 { + return nil, false + } + } + return withoutTrailer(out[i:]) +} + +// pngEnd is the IEND chunk every PNG ends with: no data, and its CRC. +var pngEnd = []byte{0, 0, 0, 0, 'I', 'E', 'N', 'D', 0xAE, 0x42, 0x60, 0x82} + +// withoutTrailer is png without text written after it. The Android +// Automotive 15 emulator printed "FORTIFY: pthread_mutex_lock called on a +// destroyed mutex" after 38 of 39 captures, and the files kept it +// (CHALLENGES 408). Only a short line of text is cut, as with a prefix; a +// capture with no IEND is left as it is. +func withoutTrailer(png []byte) ([]byte, bool) { + j := bytes.LastIndex(png, pngEnd) + if j < 0 { + return png, true + } + tail := png[j+len(pngEnd):] + if len(tail) == 0 { + return png, true } - prefix := out[:i] - if i > maxVendorPrefix || prefix[len(prefix)-1] != '\n' || bytes.IndexByte(prefix, 0) >= 0 { + if len(tail) > maxVendorPrefix || bytes.IndexByte(tail, 0) >= 0 { return nil, false } - return out[i:], true + return png[:j+len(pngEnd)], true } // Tap touches a point in device pixels. diff --git a/internal/mobiumdriver/screencap_test.go b/internal/mobiumdriver/screencap_test.go index 2450996..d8fc385 100644 --- a/internal/mobiumdriver/screencap_test.go +++ b/internal/mobiumdriver/screencap_test.go @@ -36,3 +36,31 @@ func TestPNGInSkipsAVendorLine(t *testing.T) { } } } + +// The trailer is what the Android Automotive 15 emulator printed after 38 of +// 39 captures in one test run, 2026-11-11. +func TestPNGInCutsAVendorLineAfterIt(t *testing.T) { + png := append(append(append([]byte(nil), pngMagic...), "\x00\x00\x00\rIHDR"...), pngEnd...) + trailer := []byte("FORTIFY: pthread_mutex_lock called on a destroyed mutex (0x7ad10360c8)\n") + cases := []struct { + name string + in []byte + ok bool + }{ + {"a whole capture", png, true}, + {"the car's line after it", append(append([]byte(nil), png...), trailer...), true}, + {"lines before and after it", append(append([]byte("[Warning] Multiple displays were found\n"), png...), trailer...), true}, + {"binary after it", append(append([]byte(nil), png...), "\x00\x01junk"...), false}, + {"too much after it", append(append([]byte(nil), png...), strings.Repeat("x", maxVendorPrefix+1)...), false}, + } + for _, c := range cases { + got, ok := pngIn(c.in) + if ok != c.ok { + t.Errorf("%s: ok=%v, want %v", c.name, ok, c.ok) + continue + } + if ok && !bytes.Equal(got, png) { + t.Errorf("%s: got %d bytes, want the PNG's %d", c.name, len(got), len(png)) + } + } +}