verifyfirst

The file's contents

You read the config, the stylesheet, or the source and confirmed it says the right thing.

What it captures

Authored intent, at the moment you read it.

What it cannot see

Known failures · 14

NS-003 observedcascade-override

A later cascade rule silently revokes position: fixed

reads as
The element is declared `position: fixed` in the stylesheet. Conclusion drawn: the browser or the viewport is at fault.
actually
A rule added later for an unrelated purpose (`main, .nav, .colophon { position: relative }`) matched the same element and won on document order.
blind because
Reading the intended declaration confirms the intent, never the outcome. The stylesheet says fixed; the element is relative.
the check
getComputedStyle(el).position — the resolved value, never the authored one.
cost of missing
The property is re-declared, re-prefixed and re-tested while the overriding rule stays untouched.
generalises to
Any last-writer-wins system: CSS, environment variables, layered configuration, merged dictionaries.
NS-006 observedwrong-object-correct-shape

Scraping the largest asset returns a recommendation, not the subject

reads as
A parser extracts an image from the page and it is a valid, plausible image. Conclusion drawn: extraction succeeded.
actually
The page embeds related items alongside the subject. Ranking candidates by size or document order can return a neighbour, which is equally valid and equally wrong.
blind because
Both results are real images from the correct domain. Nothing about the artifact reveals it is the wrong one.
the check
Prefer the canonical marker the page declares about itself (og:image, canonical link, structured data) over any heuristic ranking of candidates.
cost of missing
Silent substitution. Detected only when two different inputs return the same output.
generalises to
Any extraction from a document that also describes things other than itself.
NS-023 documentedfirst-writer-wins

sshd takes the first value for a keyword, so an appended directive loses to an include

reads as
/etc/ssh/sshd_config ends with `PasswordAuthentication no`, and sshd reloaded without error. Conclusion drawn: password logins are disabled.
actually
The man page states that unless noted otherwise, for each keyword the first obtained value will be used. On a stock Ubuntu image `Include /etc/ssh/sshd_config.d/*.conf` sits at line 12 of a 131-line file, so a drop-in such as 50-cloud-init.conf that sets the same keyword is read first and wins. The line appended at the bottom is parsed and discarded.
blind because
The file says what was intended, and it is the file that was edited. Precedence is a property of the merge order across several files, and the include that pre-empts the edit sits above it, out of the region being read.
the check
`sudo sshd -T | grep -i passwordauthentication` prints the effective merged value the daemon will use, which differs from the authored line whenever an earlier occurrence won. Run it with root privileges: as an unprivileged user it silently omits unreadable drop-ins.
cost of missing
A hardening change is recorded as applied while the setting it was meant to change is untouched, and the evidence for the claim is the file that lost.
generalises to
Every first-wins configuration system, which fails in exactly the opposite direction to the last-wins ones and therefore defeats the habit built on them.
source
man.openbsd.org
NS-024 documentedrendering-diverges-from-bytes

Bidirectional control characters make source read differently than it compiles

reads as
A reviewer reads the diff and the early return is plainly inside a comment. Conclusion drawn: the change is inert.
actually
Unicode bidirectional overrides (U+202A to U+202E, U+2066 to U+2069) reorder the display of tokens without changing their logical order. Compilers and interpreters adhere to the logical ordering of source code, not the visual order, so the code executed is not the code rendered. Catalogued as CVE-2021-42574, with a homoglyph variant as CVE-2021-42694.
blind because
Reading a file means reading a rendering of it. The terminal, the editor and the diff viewer all apply the same bidi algorithm as the attack, so the instrument and the exploit agree with each other and disagree with the compiler.
the check
Search for the characters instead of reading the text: `grep -rlP '[\x{202A}-\x{202E}\x{2066}-\x{2069}]' path/` names files containing them and prints nothing for files that do not. Verified against a planted sample and a clean file.
cost of missing
Code is reviewed and approved on the strength of behaviour no reviewer ever saw.
mitigation
Compilers now detect this where asked: rustc's text_direction_codepoint_in_literal lint and gcc's -Wbidi-chars. Enable them rather than relying on reading.
generalises to
Any check performed on a rendering of an artifact rather than on its bytes.
source
trojansource.codes
NS-025 documentedsilent-precision-loss

A JSON integer above 2^53 is silently rounded when parsed as a double

reads as
The response contains `"id": 10765432100123456789`; the parsed object has an id of the right shape and it round-trips through the code. Conclusion drawn: the identifier was carried through intact.
actually
JavaScript parses JSON numbers as IEEE 754 doubles. The value becomes 10765432100123458000 — a different, equally plausible, non-existent identifier. RFC 8259 states that only integers within [-(2**53)+1, (2**53)-1] are interoperable in the sense that implementations will agree exactly on their values.
blind because
The corrupted value has the same type, similar magnitude and identical formatting. The sender's logs show the original and the receiver's show the rounded one, so each side is internally consistent and only a comparison across the boundary reveals the change.
the check
`Number.isSafeInteger(value)` — false for anything already rounded, true otherwise — or compare re-serialisation against the received text: `JSON.stringify(JSON.parse(s)) === s`. Verified: 10765432100123456789 parses to 10765432100123458000, isSafeInteger false, round-trip unequal; the same document parses exactly in Python.
cost of missing
Reads and writes land on the wrong record or on none. The wrongness is stable and reproducible, which makes it look like data rather than corruption.
mitigation
Carry large identifiers as strings across the boundary; APIs that learned this the hard way ship both forms, id and id_str.
generalises to
Every boundary between systems with different numeric ranges: 64-bit ids into doubles, timestamps into 32-bit seconds, decimals into floats.
source
rfc-editor.org
NS-046 documentedimplicit-type-coercion

An unquoted YAML scalar becomes a boolean before anything reads it

reads as
config.yml reads `country: NO` and `version: 1.10`, and it is the file the service loads. Conclusion drawn: those are the values the service has.
actually
The YAML 1.1 boolean resolver matches y, yes, n, no, true, false, on and off in any capitalisation, and the loaders in widest use implement it. `NO` loads as False, `off` as False, `on` as True. Numeric resolution is equally implicit: `1.10` becomes the float 1.1 and `0xdeadbeef` becomes 3735928559.
blind because
Reading the file confirms the characters, and the characters are right. The conversion happens inside the loader, and the loaded value is never displayed beside the text it came from.
the check
Load it and print the types instead of reading it: `python3 -c "import yaml,sys;[print(repr(k),repr(v),type(v).__name__) for k,v in yaml.safe_load(open(sys.argv[1])).items()]" config.yml`. Observed on PyYAML 6.0.1: `NO` -> False, `off` -> False, `on` -> True, `1.10` -> 1.1 (float), `0xdeadbeef` -> 3735928559 (int), while `08` stayed the string '08' because it is not a valid octal literal — so neighbouring keys in one file resolve inconsistently.
cost of missing
A country code becomes a boolean and a version becomes a different version. Both are valid values of the wrong type, and both survive any review conducted by reading the file.
mitigation
Quote every scalar whose type matters. Loaders following the YAML 1.2 core schema resolve fewer of these, which changes the set of surprises rather than removing it.
generalises to
Every format that infers type from spelling: spreadsheet imports turning identifiers into dates, shell word-splitting, environment variables parsed as numbers.
source
yaml.org
NS-047 documentedrecorded-as-a-reference

A directory containing a .git is committed as a pointer rather than as files

reads as
The files are in the working tree, `git add -A` and `git commit` both succeeded, and `git status` reports a clean tree. Conclusion drawn: the files are in the repository.
actually
A directory with its own .git is recorded as a gitlink — one index entry of mode 160000 holding a commit id — not as the files beneath it. The commit id names an object in a repository nobody else can reach, and no .gitmodules entry is created. A clone contains the path as an empty directory.
blind because
git status compares the working tree with the index, and the index is satisfied: the gitlink matches the nested repository's HEAD. Everything tracked is up to date, and the untracked files sit behind a boundary status does not cross. The warning appears once, at add time, on stderr, and does not affect the exit status.
the check
Ask whether the specific file is tracked: `git ls-files --error-unmatch vendor/widget/index.js` — prints the path and exits 0 when it is, prints "did not match any file(s) known to git" and exits 1 when it is not. Observed here: `git ls-tree HEAD vendor/` returned `160000 commit bdf3631e... vendor/widget`, and a fresh clone of the repository contained README.md and nothing else.
cost of missing
The backup, the mirror or the deploy artefact is missing a subtree that every local check confirms is present. It is discovered on a clean clone, usually on another machine, usually once the original is gone.
generalises to
Every container that stores a reference where the reader assumes contents: symlinks inside archives, submodule pointers, lockfiles naming versions that no longer resolve.
source
git-scm.com
NS-048 documentedsame-name-different-file

The module that imports is the first file on the path with that name

reads as
config.py was edited, saved, and re-read to confirm the new value; the program still uses the old one. Conclusion drawn: a caching problem, or the process was not restarted.
actually
The interpreter searches sys.path in order, with the directory containing the input script placed at the front. Any file of that name in the script's directory, in the working directory, or earlier in the path is imported instead. The edited file is never read, and nothing is raised because the module that was found is a perfectly valid module.
blind because
The file being read and the file being imported have the same name and a similar shape, and the import statement names neither directory. Nothing in the source distinguishes them.
the check
Ask the imported module where it came from, invoked exactly as the program is invoked: `python3 -c 'import config; print(config.__file__)'`. Observed here with two config.py files present: a script in sub/ loaded sub/config.py and reported that path, while the copy that had been edited sat one directory up, untouched.
cost of missing
Edits accumulate in a file the program has never loaded, and the conclusion drawn concerns caching or process lifetime rather than identity, sending the work into restarts and clearing __pycache__.
generalises to
Every ordered resolution path where names are not unique: PATH, LD_LIBRARY_PATH, node_modules resolution, classpath, include directories.
source
docs.python.org
NS-060 documentedexcluded-without-a-message

git add says nothing when a pathspec matches only ignored files

reads as
`git add .` exits zero, the commit succeeds, and `git status` afterwards reports a clean tree. Conclusion drawn: everything in the working directory is committed.
actually
git-add documents both branches: 'The git add command will not add ignored files by default. You can use the --force option to add ignored files. If you specify the exact filename of an ignored file, git add will fail with a list of ignored files. Otherwise it will silently ignore the file.' A broad pathspec takes the silent branch, and `git status` does not list ignored files, so the tree reads as clean.
blind because
Both instruments agree, and both are answering a narrower question than the one asked. `git status` compares the index against the tracked working tree; a file that is neither tracked nor reportable is outside that comparison by construction.
the check
Ask whether a specific path is excluded, and list what was excluded: `git check-ignore -v path` and `git status --short --ignored`. Observed on git 2.43.0 with a .gitignore containing dist/, *.local and config/*: `git add .` exited 0, `git status --short` listed only .gitignore and app.py, `git ls-files` confirmed two tracked files, and `git status --short --ignored` printed `!! config/`, `!! dist/` and `!! settings.local` for the three that were never staged.
cost of missing
A build output, a migration or a generated asset that a broad rule happens to match is absent from every clone and every deploy, and each step in the chain reports the tree as clean.
generalises to
Every filter applied before a report is produced: exclude rules in backups and syncs, packaging manifests, .dockerignore, test collection patterns.
source
git-scm.com
NS-061 documentedreaders-disagree-about-the-bytes

Two readers of one CRLF file disagree about where each value ends

reads as
cat .env prints `API_URL=https://api.example.com` and that is the file the service loads. Conclusion drawn: the service has that URL.
actually
The file uses CRLF terminators. Python's default text mode translates them, so a Python reader sees a 23-character value; the shell does not translate, so `. ./.env` yields a 24-character value ending in a carriage return. The same bytes become different strings depending on who reads them.
blind because
Reading the file with cat, an editor or a code review renders the carriage return as nothing at all. It has no glyph, occupies no column, and is removed by several of the tools used to inspect it.
the check
Make the terminators visible, or measure the value in the reader that matters: `cat -A .env`. Observed on this host: cat -A printed `API_URL=https://api.example.com^M$`, `file` reported 'ASCII text, with CRLF line terminators', bash reported ${#API_URL} as 24 and the equality test against the intended URL failed, while Python text mode reported 23 and the same file opened in binary mode yielded 'https://api.example.com\r'.
cost of missing
A hostname, token or path acquires an invisible trailing byte. Comparisons fail, signatures do not verify, and a request goes to a name that differs from the one on screen, while every review of the file keeps confirming the value is right.
mitigation
Normalise on ingest with a gitattributes `text` rule, and compare lengths rather than appearances when a value refuses to match something it visibly equals.
generalises to
Every character that renders as nothing: byte-order marks, zero-width spaces, non-breaking spaces pasted from documents, trailing whitespace in secrets.
source
git-scm.com
NS-062 documentedbackup-and-original-are-one-object

Copying a symlink in archive mode produces a second link, not a backup

reads as
`cp -a app.conf app.conf.bak` exits zero and a listing shows both names. Conclusion drawn: the original is preserved, so the edit is safe.
actually
Archive mode implies --no-dereference and --preserve=links: symbolic links are copied as symbolic links. app.conf was a link, so app.conf.bak is a second link to the same target. There is one file. Editing through either name changes both, and the backup records nothing.
blind because
Reading either path returns the intended contents, and a listing shows two entries with the expected names. Only the link marker and the inode number distinguish a backup from an alias.
the check
Compare inodes after dereferencing: `stat -Lc '%i %n' app.conf app.conf.bak`. Observed on GNU coreutils: after `cp -a app.conf app.conf.bak` both names and the underlying real.conf reported inode 2142629, and overwriting app.conf with new content changed the contents visible through app.conf.bak at the same moment.
cost of missing
The rollback path does not exist, and its absence is discovered only when it is needed. A policy requiring a backup before editing is satisfied on paper by an operation that made none.
mitigation
`cp -L` copies the target's contents. Checking the inode immediately afterwards costs one command and is the only cheap moment to find this.
generalises to
Every duplication that may preserve a reference instead of the data: hard links, copy-on-write clones, container image layers, object-store copies that alias.
source
gnu.org
NS-083 documentededit-replaces-the-link-not-the-target

An in-place edit of a symlink replaces the link with a regular file

reads as
`sed -i 's/old/new/' app.conf` exits zero and reading app.conf shows the new value. Conclusion drawn: the configuration was updated.
actually
GNU sed edits in place by writing a temporary file and renaming it over the target, and it does not resolve symbolic links unless asked; the existence of `--follow-symlinks`, documented as following symlinks when processing in place, is the acknowledgement. The rename replaces the link itself. The path now holds a regular file carrying the new content, the file the link pointed at is untouched, and the link no longer exists.
blind because
Reading the path returns the edited content, because the path genuinely holds it now. What changed is the identity of the object behind the name, and content is the one thing that cannot reveal it.
the check
Look at the type and inode behind the name rather than at the bytes: `stat -c '%i %F %N' app.conf`. Observed on GNU sed 4.9 with app.conf a symlink to repo/app.conf: beforehand `2659981 symbolic link 'app.conf' -> 'repo/app.conf'`; after `sed -i`, `2659983 regular file 'app.conf'` holding `setting=new`, while repo/app.conf still held `setting=old` at its original inode 2659980. With `--follow-symlinks` the link survived and repo/app.conf received the edit.
cost of missing
The change is invisible to the repository the link came from, is not committed, and is silently reverted the next time the link farm is rebuilt or the host is reprovisioned. Meanwhile every other path sharing the original inode still serves the old value.
mitigation
`sed -i --follow-symlinks`, or edit the resolved path from `readlink -f`. The same applies to any tool that writes by rename.
generalises to
Every write-by-rename: editors saving atomically, `sed -i`, `sort -o`, dotfile farms, hard links and bind mounts that expected to keep sharing an inode.
source
man7.org
NS-084 documentedchange-invisible-to-the-comparison

A file that changed without changing size or timestamp is never transferred

reads as
`rsync -a src/ dst/` exits zero, and dst/app.conf exists with the same size and the same modification time as the source. Conclusion drawn: the destination is a copy of the source.
actually
rsync(1): rsync finds files that need to be transferred using a 'quick check' algorithm (by default) that looks for files that have changed in size or in last-modified time. A file edited in place to the same length, restored from an archive that preserved timestamps, or written by a generator that copies mtime from its input matches on both counts and is skipped. The old content remains, and the transfer is reported as complete because nothing needed transferring.
blind because
The two numbers the tool decides on are the two numbers used to verify it. Size and mtime agree, which is a true statement about the metadata and a false one about the contents.
the check
Compare contents rather than metadata: `rsync -ain --checksum src/ dst/` lists exactly what a content comparison would move. Observed on rsync 3.2.7 with src/app.conf holding VERSION=2 and dst/app.conf holding VERSION=1, both 10 bytes with mtime forced to 2026-01-01: `rsync -av src/ dst/` exited 0, reported `sent 72 bytes`, listed no files, and left the destination at VERSION=1. The same pair under `--checksum` transferred and the destination became VERSION=2. `cp -u` copied nothing for the same reason.
cost of missing
A deploy or backup succeeds and changes nothing, and repeating it reproduces the same success, so the obvious response to the symptom confirms the wrong hypothesis.
mitigation
Use `--checksum` for any sync where the content is authoritative and the timestamps are not, accepting the read cost; or ensure the writer touches mtime whenever it rewrites.
generalises to
Every change detector keyed on a proxy for content: mtime-based build systems, ETags derived from metadata, cache keys built from a version string that was not bumped.
source
man7.org
NS-085 documentedduplicate-name-silently-resolved

A repeated key is resolved silently, and the occurrence you read is not the one in force

reads as
config.json declares `"debug": false` and a database host of prod.db.internal, and it is the file the service loads. Conclusion drawn: debug is off and the service talks to production.
actually
The same names appear again further down the file, added by a later edit or a careless merge. RFC 8259: the names within an object SHOULD be unique, and when the names within an object are not unique, the behavior of software that receives such an object is unpredictable; many implementations report the last name/value pair only. The values in force are the ones at the bottom.
blind because
Reading a configuration file means reading downwards and stopping at the first occurrence of the key in question. Nothing in the syntax marks a name as later overridden, and both occurrences are individually valid.
the check
Load it through the parser the service uses and print what it produced: `python3 -c 'import json, sys; print(json.load(open(sys.argv[1])))' config.json`. Observed on Python 3.12.3, Node 20.20.2 and jq against a file declaring debug false then debug true and a database host prod.db.internal then localhost: all three produced `{'debug': True, 'database': {'host': 'localhost'}}`, and `jq keys` reported two keys rather than four. PyYAML 6.0.1 behaved the same way on the YAML equivalent. Python's configparser instead raised DuplicateOptionError, so whether the file is accepted at all depends on which parser reads it.
cost of missing
Debug output, a staging database or a permissive CORS origin is live in production, and the file that proves otherwise is the same file that enables it.
mitigation
Reject duplicates at load time; Python's `json.load` accepts an `object_pairs_hook` that can raise on a repeated name, and most YAML loaders can be configured to do the same.
generalises to
Every last-writer-wins merge that leaves both writers visible: duplicate keys, repeated environment assignments, layered configuration, stylesheet rules of equal specificity.
source
rfc-editor.org

Plain text: /file-on-disk.txt · all instruments