commit 6c739b82dac5cdf1c2e3b36151b41b4767f69db1
parent 8b69178513d4f42a3f2645d66adf23d4a7b6b713
Author: Jared Tobin <jared@jtobin.io>
Date: Mon, 19 Oct 2026 13:42:14 +0400
diff: outcome order compared, and runs that prove nothing refused
The QEMU plugin now keeps, for each instruction, an order-sensitive
fingerprint of the instructions that followed it, and the diff
compares it. A branch that runs as often, and takes each of its
outcomes as often, in two runs, but in another order, is now a
control origin. Counting outcomes alone missed it, so a site memcheck
saw the secret reach, and that no run appeared to vary, could be
called steady when it was not.
A taint run that marked no secret is refused. The first secret mark
of any bytes is what writes the loaded objects, so an empty objects
file means nothing was secret, and a clean report of such a run said
nothing about the code.
Runs are made repeatable, or refused. QEMU's user mode answered a
guest's getrandom from the host, which -seed does not reach, so two
runs of one secret could differ wherever the program computed on its
entropy; those instructions were set aside as unsteady, together
with anything at them that also depended on the secret. vestigia now
builds QEMU's user mode alone, patched to draw getrandom from the
seeded generator. record refuses to run when it cannot turn address
randomisation off, rather than warning and comparing runs laid out
differently.
The README states the contract for designed runs plainly: the same
harness and arguments, only the secret, chosen to reach a path.
Diffstat:
9 files changed, 160 insertions(+), 77 deletions(-)
diff --git a/CHANGELOG b/CHANGELOG
@@ -13,11 +13,14 @@
with vestigia's plugin, its leakage trace summarised per
instruction, and where runs differing only in the secret
diverge (vestigia/diff-v2: each origin with the runs alike
- there and the runs that never reached it, and what ran
+ there and the runs that never reached it, a branch taking its
+ outcomes in another order an origin too, and what ran
downstream listed per object), set against controls repeating a
secret, with what every run executed in each object, and runs
designed to reach particular paths compared for control and
coverage alone (`--designed`); runs are recorded with
- randomisation off and the guest's entropy seeded.
+ randomisation off (or not at all) and the guest's entropy,
+ its getrandom included, seeded. A taint run that marked no
+ secret is refused.
* `traceOf` and `diffOf` run either as a derivation;
`nix/lib.nix` holds them for any package set.
diff --git a/README.md b/README.md
@@ -45,9 +45,11 @@ The harness compiles `include/vestigia.c` beside it, which defines
the marks; a harness that reaches them through a foreign function
interface (a Haskell `foreign import ccall`) compiles the same file.
Outside valgrind the marks are no-ops, so the same harness can be
-timed by censor unchanged. On Linux the first secret mark also
-writes where the loader put each object, for `vestigia run` to name
-every site by its object and its address in it.
+timed by censor unchanged. On Linux the first secret mark of any
+bytes also writes where the loader put each object, for `vestigia
+run` to name every site by its object and its address in it. A run
+that marked nothing found nothing because nothing was secret, and
+`run` refuses it.
### Taint: `vestigia run`
@@ -99,7 +101,8 @@ address 0x152eb Fq::sqrt_ratio_zeta invsqrt.rs:117 ...
```
An *origin* is a branch whose outcomes differ in proportion between
-runs (a loop whose trip count the secret sets is one) or an
+runs (a loop whose trip count the secret sets is one), a branch
+taking the same outcomes as often in another order, or an
instruction run equally often whose addresses differ. Each origin
names the runs that behaved alike there, grouped, and the runs that
never reached it, each run by its dump's file name: which secrets
@@ -111,18 +114,24 @@ chosen secrets exercised, to set against what the code could
execute. `--object` keeps origins, what ran downstream, and what
was executed, to objects whose path contains the given text.
-`--designed DUMP` names a run aimed at a particular path. It differs
-from the others in where it goes, not only in the secret, so it
-counts toward the control origins (a branch whose outcomes shift
-under it chose by the secret) and what was executed, never toward
-the address origins, which only runs alike in all but the secret can
-show.
+`--designed DUMP` names a run aimed at a particular path: the same
+harness with the same arguments, differing from the others only in a
+secret chosen to reach that path. A run whose public inputs differ
+is not designed but another experiment, and its differences say
+nothing about the secret. A designed run counts toward the control
+origins (a branch whose outcomes shift under it chose by the
+secret) and what was executed, never toward the address origins:
+it goes where the others do not, so an instruction's addresses
+differing in it need not mean an address computed from the secret.
A difference means the secret only if nothing else differs between
runs. `record` turns address-space randomisation off for itself and
-QEMU, and seeds the guest's entropy, so runs lay memory out alike
-and a hash table seeded from the system's randomness is laid out
-alike too. `--control DUMP` names a run repeating the first run's
+QEMU, refusing to run where it cannot, and seeds the guest's
+entropy, so runs lay memory out alike and a hash table seeded from
+the system's randomness is laid out alike too. The pinned QEMU
+answers the guest's `getrandom` from that seeded generator
+(`nix/qemu-getrandom.patch`); a guest reading `/dev/urandom` still
+reads the host's. `--control DUMP` names a run repeating the first run's
secret: whatever differs between the two (a clock read, a
scheduler's choice) is *unsteady*, counted and kept out of the
origins.
diff --git a/app/Main.hs b/app/Main.hs
@@ -14,7 +14,7 @@ module Main (
) where
import Control.Exception (IOException, try)
-import Control.Monad (unless)
+import Control.Monad (unless, when)
import qualified Data.ByteString as BS
import Data.Version (showVersion)
import qualified Data.Text as T
@@ -183,6 +183,13 @@ run o prog progArgs = do
exitWith (ExitFailure 2)
Right v -> pure v
removeFile objects
+ -- no objects written, no secret marked: a run that found nothing
+ -- because nothing was secret is no evidence, and is refused
+ when (null os) $ do
+ hPutStrLn stderr "vestigia: the harness marked no secret \
+ \(vestigia_secret was never called on any bytes); its run \
+ \says nothing"
+ exitWith (ExitFailure 2)
report o (Just (exitCode code)) os xml
if temporary then removeFile xml else pure ()
where
@@ -222,9 +229,10 @@ record o prog progArgs = do
-- same entropy: a hash table seeded from the operating system's
-- randomness is laid out anew in every process
fixed <- fixLayout
- unless fixed $ hPutStrLn stderr
- "vestigia: address randomisation is still on; runs may differ \
- \in layout, and so everywhere"
+ unless fixed $ do
+ hPutStrLn stderr "vestigia: address randomisation could not be \
+ \turned off; runs would differ in layout, and so everywhere"
+ exitWith (ExitFailure 2)
qemu <- tool (oQemu o) "VESTIGIA_QEMU" "qemu-x86_64"
plugin <- tool (oPlugin o) "VESTIGIA_PLUGIN" "libvestigia-qemu.so"
r <- try (withCreateProcess
diff --git a/include/vestigia.c b/include/vestigia.c
@@ -7,11 +7,13 @@
* A site is reported at a run-time address, and only the loader
* knows where it put the object holding it. On Linux, when the
* environment names a file in VESTIGIA_OBJECTS (vestigia sets it),
- * the first secret mark writes there each loaded object's load bias
- * and path, so a site can be named by its object and its address in
- * it. That needs dl_iterate_phdr, which glibc declares only under
- * _GNU_SOURCE, set here before any header can settle the feature
- * set; this is why the marks are a compilation unit, not a header.
+ * the first secret mark of any bytes writes there each loaded
+ * object's load bias and path, so a site can be named by its object
+ * and its address in it; that the file was written is also how
+ * vestigia knows a secret was marked at all. That needs
+ * dl_iterate_phdr, which glibc declares only under _GNU_SOURCE, set
+ * here before any header can settle the feature set; this is why
+ * the marks are a compilation unit, not a header.
*/
#ifdef __linux__
#define _GNU_SOURCE
@@ -60,7 +62,7 @@ static void objects_(void) {}
#endif
void vestigia_secret(void *p, size_t n) {
- objects_();
+ if (n > 0) objects_();
(void)VALGRIND_MAKE_MEM_UNDEFINED(p, n);
}
diff --git a/lib/Vestigia/Diff.hs b/lib/Vestigia/Diff.hs
@@ -17,11 +17,13 @@
-- branch whose outcomes differ in proportion between runs had its
-- own condition depend on the secret, and is a `control` origin
-- (a loop whose trip count the secret sets is one, though it also
--- runs a different number of times). An instruction run equally
--- often in every run whose addresses differ computed one from the
--- secret: an `address` origin. Every other instruction run a
--- different number of times sits where control had already
--- diverged: it is downstream, and is counted rather than listed.
+-- runs a different number of times). So is a branch run as often,
+-- taking each transfer as often, in another order: the plugin keeps
+-- an order-sensitive fingerprint of what followed each instruction.
+-- An instruction run equally often in every run whose addresses
+-- differ computed one from the secret: an `address` origin. Every
+-- other instruction run a different number of times sits where
+-- control had already diverged: it is downstream.
--
-- Unlike memcheck's taint, this sees what control flow does to
-- data as well as what data does: a value chosen by a secret
@@ -83,6 +85,8 @@ data Insn = Insn {
, inCount :: !Integer
, inAccesses :: !Integer
, inFingerprint :: !T.Text
+ -- | what followed it, in order, fingerprinted
+ , inOrder :: !T.Text
, inObject :: !(Maybe T.Text)
, inOffset :: !Integer
} deriving (Eq, Show)
@@ -101,8 +105,9 @@ readDump t = go (T.lines t) M.empty M.empty
go ls is es = case ls of
[] -> pure (Dump is es)
(l : rest) -> case T.words l of
- [ "I", pc, n, a, fp, obj, off ] -> do
+ [ "I", pc, n, a, fp, ord, obj, off ] -> do
i <- Insn <$> unhex pc <*> unhex n <*> unhex a <*> pure fp
+ <*> pure ord
<*> pure (if obj == "-" then Nothing else Just obj)
<*> unhex off
go rest (M.insert (inPc i) i is) es
@@ -189,7 +194,7 @@ diffDesigned cs ds xs = Divergence {
ta = M.lookup pc (dpTransfers a)
tb = M.lookup pc (dpTransfers b)
, fmap sig ia /= fmap sig ib || ta /= tb ]
- sig i = (inCount i, inFingerprint i)
+ sig i = (inCount i, inFingerprint i, inOrder i)
origins = [ o | o@(_, i) <- controls <> addresses
, not (inPc i `S.member` unsteady) ]
@@ -202,9 +207,9 @@ diffDesigned cs ds xs = Divergence {
-- declared runs speak to an address.
behaviourAt o pc = case o of
ControlOrigin ->
- ( classes proportional
+ ( classes outcomesAlike
[ (k, (inCount i, M.findWithDefault M.empty pc
- (dpTransfers d)))
+ (dpTransfers d), inOrder i))
| (k, d, i) <- ranIn runs ]
, absentIn runs )
AddressOrigin ->
@@ -236,10 +241,11 @@ diffDesigned cs ds xs = Divergence {
steadyDeclared = S.filter (alike . countsIn ds)
(S.unions (fmap (M.keysSet . dpInsns) ds))
- -- an instruction's run count and transfers, in each run that ran
- -- it
+ -- an instruction's run count, transfers, and the order of what
+ -- followed it, in each run that ran it
ranAt pc =
- [ (inCount i, M.findWithDefault M.empty pc (dpTransfers d))
+ [ ( inCount i, M.findWithDefault M.empty pc (dpTransfers d)
+ , inOrder i )
| d <- runs, Just i <- [ M.lookup pc (dpInsns d) ]
, inCount i > 0 ]
transferSources = S.unions (fmap (M.keysSet . dpTransfers) runs)
@@ -250,8 +256,12 @@ diffDesigned cs ds xs = Divergence {
M.keysSet ts == M.keysSet ts'
&& and (M.elems (M.intersectionWith (\e e' -> e * c' == e' * c)
ts ts'))
+ -- outcomes alike: each target taken in the same proportion and,
+ -- where it ran as often, in the same order
+ outcomesAlike (c, ts, o) (c', ts', o') =
+ proportional (c, ts) (c', ts') && (c /= c' || o == o')
divergent pc = case ranAt pc of
- (r : rest) -> not (all (proportional r) rest)
+ (r : rest) -> not (all (outcomesAlike r) rest)
[] -> False
controlPcs = S.filter divergent transferSources
diff --git a/nix/lib.nix b/nix/lib.nix
@@ -22,8 +22,14 @@ let
# the QEMU plugin recording a run's leakage trace per
# instruction, and the QEMU that runs a harness of this
- # system's ISA under it
- qemu = pkgs.qemu;
+ # system's ISA under it: user mode alone, its guest's getrandom
+ # drawn from the generator -seed fixes (qemu-getrandom.patch)
+ qemu = (pkgs.qemu.override {
+ userOnly = true;
+ hostCpuTargets = [ "x86_64-linux-user" "aarch64-linux-user" ];
+ }).overrideAttrs (o: {
+ patches = (o.patches or [ ]) ++ [ ./qemu-getrandom.patch ];
+ });
qemuFor = {
x86_64 = "${qemu}/bin/qemu-x86_64";
aarch64 = "${qemu}/bin/qemu-aarch64";
diff --git a/nix/qemu-getrandom.patch b/nix/qemu-getrandom.patch
@@ -0,0 +1,22 @@
+QEMU's user mode answers a guest's getrandom from the host, which
+-seed does not reach: two runs of one program draw different entropy,
+and every instruction computing on it differs between them. Draw it
+from the guest generator instead, which -seed makes deterministic and
+which, unseeded, draws from the host as before.
+
+--- a/linux-user/syscall.c
++++ b/linux-user/syscall.c
+@@ -10898,7 +10898,12 @@
+ if (!p) {
+ return -TARGET_EFAULT;
+ }
+- ret = get_errno(getrandom(p, arg2, arg3));
++ /*
++ * The guest's entropy, from the generator -seed makes
++ * deterministic; unseeded, it draws from the host as before.
++ */
++ qemu_guest_getrandom_nofail(p, arg2);
++ ret = arg2;
+ unlock_user(p, arg1, ret);
+ return ret;
+ #endif
diff --git a/plugin/vestigia-qemu.c b/plugin/vestigia-qemu.c
@@ -7,18 +7,20 @@
*
* For every guest instruction executed it records how many times it
* ran, every non-sequential transfer out of it (a branch taken, a
- * call, a jump, a return) with its count, and an order-sensitive
- * fingerprint of the data addresses it touched. Two runs of a
- * program that differ only in a secret then differ exactly where
- * the execution depended on the secret: a transfer whose counts
- * differ is a secret-dependent branch, and an instruction run as
+ * call, a jump, a return) with its count, an order-sensitive
+ * fingerprint of the data addresses it touched, and an order-
+ * sensitive fingerprint of the instructions that followed it. Two
+ * runs of a program that differ only in a secret then differ exactly
+ * where the execution depended on the secret: a transfer whose counts
+ * differ is a secret-dependent branch, as is one taking the same
+ * transfers as often in another order, and an instruction run as
* often but touching other addresses computed one from the secret.
* Nothing needs aligning, and no taint is propagated, so effects of
* control flow are seen as well as those of data flow.
*
* At exit it writes, to the file named by its `out` argument:
*
- * I <pc> <count> <accesses> <fingerprint> <object> <offset>
+ * I <pc> <count> <accesses> <fingerprint> <order> <object> <offset>
* E <from> <to> <count>
*
* in hex, where <object> is the file the instruction's bytes were
@@ -43,6 +45,7 @@ typedef struct Insn {
uint64_t count;
uint64_t accesses;
uint64_t fingerprint;
+ uint64_t order; /* the successors, in the order they followed */
GHashTable *succ; /* target pc -> count, for transfers */
} Insn;
@@ -61,6 +64,7 @@ static Insn *insn_at(uint64_t pc, uint64_t size, uintptr_t haddr) {
i->size = size;
i->haddr = haddr;
i->fingerprint = fnv_offset;
+ i->order = fnv_offset;
i->succ = g_hash_table_new_full(g_int64_hash, g_int64_equal,
g_free, g_free);
g_hash_table_insert(insns, &i->pc, i);
@@ -82,8 +86,10 @@ static void transfer(Insn *from, uint64_t to) {
static void on_exec(unsigned int vcpu, void *udata) {
Insn *i = udata;
(void)vcpu;
- if (prev != NULL && i->pc != prev->pc + prev->size)
- transfer(prev, i->pc);
+ if (prev != NULL) {
+ prev->order = (prev->order ^ i->pc) * fnv_prime;
+ if (i->pc != prev->pc + prev->size) transfer(prev, i->pc);
+ }
prev = i;
i->count++;
}
@@ -169,12 +175,13 @@ static void at_exit(qemu_plugin_id_t id, void *p) {
const Region *r = region_of(rs, i->haddr);
if (r != NULL)
fprintf(out, "I %" PRIx64 " %" PRIx64 " %" PRIx64 " %016" PRIx64
- " %s %" PRIx64 "\n", i->pc, i->count, i->accesses,
- i->fingerprint, r->path,
+ " %016" PRIx64 " %s %" PRIx64 "\n", i->pc, i->count,
+ i->accesses, i->fingerprint, i->order, r->path,
(uint64_t)(i->haddr - r->start) + r->offset);
else
fprintf(out, "I %" PRIx64 " %" PRIx64 " %" PRIx64 " %016" PRIx64
- " - 0\n", i->pc, i->count, i->accesses, i->fingerprint);
+ " %016" PRIx64 " - 0\n", i->pc, i->count, i->accesses,
+ i->fingerprint, i->order);
GHashTableIter jt;
gpointer tk, tn;
g_hash_table_iter_init(&jt, i->succ);
diff --git a/test/Main.hs b/test/Main.hs
@@ -154,9 +154,9 @@ diffTests = testGroup "diff"
-- a repeat of A whose 0x20 touched other addresses: an
-- entropy source, not the secret
c <- either assertFailure pure (readDump (T.unlines
- [ "I 10 4 0 aaaa /lib.so 10", "E 10 30 2"
- , "I 20 4 4 ffff /lib.so 20", "I 30 2 0 cccc /lib.so 30"
- , "I 40 1 1 dddd /lib.so 40" ]))
+ [ "I 10 4 0 aaaa 0 /lib.so 10", "E 10 30 2"
+ , "I 20 4 4 ffff 0 /lib.so 20", "I 30 2 0 cccc 0 /lib.so 30"
+ , "I 40 1 1 dddd 0 /lib.so 40" ]))
let dv = diffControlled [ c ] [ a, b ]
fmap (\(o, i) -> (o, inPc i)) (dvOrigins dv)
@?= [ (ControlOrigin, 0x10) ]
@@ -168,9 +168,9 @@ diffTests = testGroup "diff"
-- a run designed onto another path: 0x10 taken every time, and
-- 0x20's addresses its own
x <- either assertFailure pure (readDump (T.unlines
- [ "I 10 4 0 aaaa /lib.so 10", "E 10 30 4"
- , "I 20 4 4 9999 /lib.so 20", "I 30 4 0 cccc /lib.so 30"
- , "I 50 1 0 eeee /lib.so 50" ]))
+ [ "I 10 4 0 aaaa 0 /lib.so 10", "E 10 30 4"
+ , "I 20 4 4 9999 0 /lib.so 20", "I 30 4 0 cccc 0 /lib.so 30"
+ , "I 50 1 0 eeee 0 /lib.so 50" ]))
let dv = diffDesigned [] [ a, a ] [ x ]
fmap (\(o, i) -> (o, inPc i)) (dvOrigins dv)
@?= [ (ControlOrigin, 0x10) ]
@@ -185,8 +185,8 @@ diffTests = testGroup "diff"
a <- either assertFailure pure (readDump dumpA)
b <- either assertFailure pure (readDump dumpB)
x <- either assertFailure pure (readDump (T.unlines
- [ "I 10 4 0 aaaa /lib.so 10", "E 10 30 4"
- , "I 20 4 4 9999 /lib.so 20", "I 30 4 0 cccc /lib.so 30" ]))
+ [ "I 10 4 0 aaaa 0 /lib.so 10", "E 10 30 4"
+ , "I 20 4 4 9999 0 /lib.so 20", "I 30 4 0 cccc 0 /lib.so 30" ]))
-- at the branch, the first and third runs alike; the designed
-- run is a control origin's to group, and the address origin
-- speaks only of the declared runs
@@ -203,14 +203,30 @@ diffTests = testGroup "diff"
, testCase "downstream is kept to the objects under audit" $ do
a <- either assertFailure pure (readDump dumpA)
b <- either assertFailure pure (readDump (T.unlines
- [ "I 10 4 0 aaaa /lib.so 10", "E 10 30 3"
- , "I 20 4 4 bbbb /lib.so 20", "I 30 3 0 cccc /lib.so 30"
- , "I 40 1 1 dddd /lib.so 40", "I 60 2 0 ffff /libc.so 60" ]))
+ [ "I 10 4 0 aaaa 0 /lib.so 10", "E 10 30 3"
+ , "I 20 4 4 bbbb 0 /lib.so 20", "I 30 3 0 cccc 0 /lib.so 30"
+ , "I 40 1 1 dddd 0 /lib.so 40", "I 60 2 0 ffff 0 /libc.so 60" ]))
let dv = diffDumps [ a, b ]
fmap inPc (dvDownstream dv) @?= [ 0x30, 0x60 ]
fmap inPc (dvDownstream (restrictTo [ "lib.so" ] dv))
@?= [ 0x30 ]
+ , testCase "a branch taken as often, in another order, is an origin" $ do
+ -- 0x10 runs twice and falls through once and jumps once in both
+ -- runs, but in the other order
+ a <- either assertFailure pure (readDump (T.unlines
+ [ "I 10 2 0 aaaa 1111 /lib.so 10", "E 10 30 1"
+ , "I 20 1 0 bbbb 0 /lib.so 20", "I 30 1 0 cccc 0 /lib.so 30" ]))
+ b <- either assertFailure pure (readDump (T.unlines
+ [ "I 10 2 0 aaaa 2222 /lib.so 10", "E 10 30 1"
+ , "I 20 1 0 bbbb 0 /lib.so 20", "I 30 1 0 cccc 0 /lib.so 30" ]))
+ fmap (\(o, i) -> (o, inPc i)) (dvOrigins (diffDumps [ a, b ]))
+ @?= [ (ControlOrigin, 0x10) ]
+ M.lookup (ControlOrigin, 0x10) (dvBehaviour (diffDumps [ a, b, a ]))
+ @?= Just ([ [ 0, 2 ], [ 1 ] ], [])
+ -- in the same order, nothing
+ dvOrigins (diffDumps [ a, a ]) @?= []
+
, testCase "identical runs have no origins" $ do
a <- either assertFailure pure (readDump dumpA)
let dv = diffDumps [ a, a ]
@@ -220,8 +236,8 @@ diffTests = testGroup "diff"
, testCase "what any run executed is kept per object" $ do
a <- either assertFailure pure (readDump dumpA)
b <- either assertFailure pure (readDump (T.unlines
- [ "I 10 4 0 aaaa /lib.so 10", "I 50 1 0 eeee /lib.so 50"
- , "I 60 1 0 ffff /libc.so 60", "I 70 0 0 0000 /lib.so 70" ]))
+ [ "I 10 4 0 aaaa 0 /lib.so 10", "I 50 1 0 eeee 0 /lib.so 50"
+ , "I 60 1 0 ffff 0 /libc.so 60", "I 70 0 0 0000 0 /lib.so 70" ]))
let dv = diffDumps [ a, b ]
dvExecuted dv @?= M.fromList
[ ("/lib.so", S.fromList [ 0x10, 0x20, 0x30, 0x40, 0x50 ])
@@ -230,31 +246,31 @@ diffTests = testGroup "diff"
@?= [ "/lib.so" ]
, testCase "an unreadable line is refused" $
- isLeft (readDump "I 10 zz 0 f /lib.so 0\n") @? "accepted"
+ isLeft (readDump "I 10 zz 0 f 0 /lib.so 0\n") @? "accepted"
]
where
-- 0x10 is a branch run four times, taken twice in A and three
-- times in B; 0x20 reads other addresses in B; 0x30, inside the
-- branch, runs a different number of times; 0x40 is the same
dumpA = T.unlines
- [ "I 10 4 0 aaaa /lib.so 10", "E 10 30 2"
- , "I 20 4 4 bbbb /lib.so 20", "I 30 2 0 cccc /lib.so 30"
- , "I 40 1 1 dddd /lib.so 40" ]
+ [ "I 10 4 0 aaaa 0 /lib.so 10", "E 10 30 2"
+ , "I 20 4 4 bbbb 0 /lib.so 20", "I 30 2 0 cccc 0 /lib.so 30"
+ , "I 40 1 1 dddd 0 /lib.so 40" ]
dumpB = T.unlines
- [ "I 10 4 0 aaaa /lib.so 10", "E 10 30 3"
- , "I 20 4 4 eeee /lib.so 20", "I 30 3 0 cccc /lib.so 30"
- , "I 40 1 1 dddd /lib.so 40" ]
+ [ "I 10 4 0 aaaa 0 /lib.so 10", "E 10 30 3"
+ , "I 20 4 4 eeee 0 /lib.so 20", "I 30 3 0 cccc 0 /lib.so 30"
+ , "I 40 1 1 dddd 0 /lib.so 40" ]
-- a loop at 0x10 the secret runs 5 or 9 times, its back edge to
-- 0x14 taken every time but the last; 0x18, in the body, always
-- jumps back to 0x10
loopA = T.unlines
- [ "I 10 5 0 aaaa /lib.so 10", "E 10 14 4"
- , "I 14 4 0 bbbb /lib.so 14"
- , "I 18 4 0 cccc /lib.so 18", "E 18 10 4" ]
+ [ "I 10 5 0 aaaa 0 /lib.so 10", "E 10 14 4"
+ , "I 14 4 0 bbbb 0 /lib.so 14"
+ , "I 18 4 0 cccc 0 /lib.so 18", "E 18 10 4" ]
loopB = T.unlines
- [ "I 10 9 0 aaaa /lib.so 10", "E 10 14 8"
- , "I 14 8 0 bbbb /lib.so 14"
- , "I 18 8 0 cccc /lib.so 18", "E 18 10 8" ]
+ [ "I 10 9 0 aaaa 0 /lib.so 10", "E 10 14 8"
+ , "I 14 8 0 bbbb 0 /lib.so 14"
+ , "I 18 8 0 cccc 0 /lib.so 18", "E 18 10 8" ]
symbolTests :: TestTree
symbolTests = testGroup "symbol"