. "$BST_LIB" # safec (safeclib) 3.7.1 -- Reini Urban's implementation of the C11 Annex K # bounds-checked interfaces: strcpy_s, memcpy_s, strcat_s, strtok_s and the rest # of the *_s family, from meta-oe recipes-core/safec. # # --------------------------------------------------------------------------- # WHAT THIS SUITE IS ACTUALLY MEASURING, AND WHY IT CAN BE STRICT # # safec installs NO EXECUTABLE of its own that exercises the library, so without # a cross-linked consumer this suite could assert only that an archive and seven # headers exist -- which would pass just as happily if every function in the # archive trapped on first call. data/safec-probe is that consumer: the probe # source cross-linked against the very archive this port installs, reached # through the same /usr/include/safeclib path a real consumer uses. # # THE ORACLE IS A NATIVE x86_64 RUN OF THE IDENTICAL SOURCE (data/safec-native.txt), # and it is a true oracle rather than a convenience, because ANNEX K SPECIFIES # RETURN CODES, NOT BEHAVIOUR-UNDER-A-TOLERANCE. Every value the probe prints is # an errno_t code, a small count or index, a normalised comparison sign, or a hex # byte string -- never a pointer, a size_t width or a sizeof. So there is nothing # in its output that is ALLOWED to differ between the two machines, and a single # disagreement is a defect rather than a platform difference to reason about. # # THE CASES THAT EARN THE PORT ITS KEEP are the C-section refusals. The entire # value of this library is that a constraint violation is REPORTED rather than # written past the end of a buffer, so "strcpy_s refuses a source that does not # fit" (C13) and "the destination is left EMPTY after it refuses" (C14/C16/C19) # are the difference between a safe consumer and a silent overflow. A build in # which those returned EOK would be worse than not having the library at all -- # it would be an overflow wearing a safety label. # # C34/C35 are a second, DIFFERENT protection and worth calling out separately: # every other case trusts dmax and checks the data against it, whereas those two # check DMAX ITSELF against the real object size, via the __builtin_object_size # value safeclib's macros pass alongside every buffer. They catch the caller who # lies about how big the destination is. # --------------------------------------------------------------------------- # A root prefix, empty on the target and set only when this suite is validated # NATIVELY under dash before it ships -- which is how the expectations here were # checked in the first place. Writing /usr/lib and /usr/include needs real root, # so a native run stages them under a temporary tree and points this at it. The # runner never sets it, so on the image these are exactly the absolute paths. R="${BST_SAFEC_ROOT:-}" ARCHIVE=$R/usr/lib/libsafec.a INCDIR=$R/usr/include/safeclib PCFILE=$R/usr/lib/pkgconfig/libsafec.pc PROBE=$BST_DATA/safec-probe NATIVE=$BST_DATA/safec-native.txt if [ ! -f "$ARCHIVE" ]; then bst_skip safec/present "safec not installed on this image" exit 0 fi bst_ok safec/present # --- the package is whole ---------------------------------------------------- for h in safe_lib.h safe_str_lib.h safe_mem_lib.h safe_types.h \ safe_config.h safe_lib_errno.h safe_compile.h; do check "safec/hdr-${h%.h}" test -f "$INCDIR/$h" done check safec/staticlib test -f "$ARCHIVE" check safec/pkgconfig test -f "$PCFILE" # ${PN} is empty by design: the distro is static-only (--disable-shared reaches # this package through its configure script), so the archive is the whole # library deliverable. A .so appearing here would mean that stopped being true. if [ -e "$R/usr/lib/libsafec.so" ] || [ -e "$R/usr/lib/libsafec.so.3" ]; then bst_notok safec/no-shared "a shared libsafec is installed on a static-only distro" else bst_ok safec/no-shared fi # The archive carries the API. grep rather than nm: the image ships no binutils # nm, and an ar symbol index holds the names as plain bytes, so this works on # busybox grep with no toolchain present. The real symbols carry safeclib's _chk # suffix -- the public names are macros that add the object-size argument. for sym in _strcpy_s_chk _strcat_s_chk _memcpy_s_chk _memset_s_chk \ _strtok_s_chk _strnlen_s_chk abort_handler_s ignore_handler_s; do if grep -q "$sym" "$ARCHIVE" 2>/dev/null; then bst_ok "safec/sym-$sym" else bst_notok "safec/sym-$sym" "$sym absent from $ARCHIVE" fi done # --- safec-check: the package's own CLI -------------------------------------- # check_for_unsafe_apis is a perl script that scans C source for unsafe calls and # names the Annex K replacement for each. It is worth covering separately from # the probe for two reasons: it is the only EXECUTABLE this package ships, and it # exercises a completely different runtime path -- the image's perl reading a # file -- rather than the archive the probe links. Its output text is fixed by # its own source, not by the target, so these expectations are the script's # documented behaviour and need no native control. CHECKER=$R/usr/bin/check_for_unsafe_apis if [ ! -f "$CHECKER" ]; then bst_skip safec/checker-present "safec-check not installed on this image" elif ! command -v perl >/dev/null 2>&1; then bst_skip safec/checker-present "perl absent — check_for_unsafe_apis cannot run" else bst_ok safec/checker-present csrc=${TMPDIR:-/tmp}/bst-safec-unsafe.$$.c printf '#include \nvoid f(char *d, const char *s) { strcpy(d, s); }\n' > "$csrc" cout=$(perl "$CHECKER" "$csrc" 2>&1); crc=$? rm -f "$csrc" if [ "$crc" -eq 0 ]; then bst_ok safec/checker-runs else bst_notok safec/checker-runs "check_for_unsafe_apis exited $crc: $(_bst_1line "$cout")"; fi # It must both FIND the unsafe call and NAME its replacement -- a scanner that # ran but flagged nothing would exit 0 and look identical in a summary line. case "$cout" in *strcpy*) bst_ok safec/checker-flags-strcpy ;; *) bst_notok safec/checker-flags-strcpy "strcpy not flagged: $(_bst_1line "$cout")" ;; esac case "$cout" in *strcpy_s*) bst_ok safec/checker-names-replacement ;; *) bst_notok safec/checker-names-replacement "strcpy_s not suggested: $(_bst_1line "$cout")" ;; esac fi # --- the probe --------------------------------------------------------------- for f in "$PROBE" "$NATIVE"; do if [ ! -f "$f" ]; then bst_skip safec/probe-runs "$(basename "$f") absent from the image" exit 0 fi done # data/* installs 0644, so stage a copy and make it executable. pout=${TMPDIR:-/tmp}/bst-safec-out.$$ _p=${TMPDIR:-/tmp}/bst-safec-$$.bin cp "$PROBE" "$_p" 2>/dev/null && chmod 0755 "$_p" 2>/dev/null if [ ! -x "$_p" ]; then bst_notok safec/probe-runs "could not stage an executable copy of the probe" rm -f "$_p" "$pout" exit 0 fi "$_p" > "$pout" 2>&1 prc=$? rm -f "$_p" if [ "$prc" -eq 0 ]; then bst_ok safec/probe-runs else bst_notok safec/probe-runs "probe exited $prc" fi # The probe prints a terminator, so a run that DIED PARTWAY is distinguishable # from one that merely disagreed. The per-case loop below reports a missing value # as "probe stopped before this case", which localises the death to one case id # instead of smearing a single trap across every remaining case. if grep -q '^PROBE-END$' "$pout" 2>/dev/null; then bst_ok safec/probe-complete else bst_notok safec/probe-complete "probe output has no PROBE-END terminator (died at $(awk 'END{print $1}' "$pout" 2>/dev/null))" fi # --- every probe case, against the native x86_64 control --------------------- # One BST case per probe case, so a disagreement names the exact construct that # differs rather than reporting "the probe output changed". The expectation is # read from the control file, never written here: nothing in this suite encodes # what a value "should" be, which is what keeps the oracle honest -- a future # edit to the probe must regenerate the control and cannot quietly move an # expectation to match a new result. # # THE JOIN IS ONE awk PASS, NOT ONE PER CASE, and that is a runtime constraint # rather than a style preference. The obvious shape -- looping over the control # and asking awk for each id in turn -- spawns a process per case, and a spawn on # wasm32 is a fresh module instantiation rather than a fork: ~150 of them took the # suite past 7 minutes and blew BST_SUITE_TIMEOUT, which reports as a FAILING # suite-timeout case and would have attributed a harness cost to safec. Joining # both files in a single pass and letting the shell read pre-computed verdicts # brings it inside the default budget; bst_ok/bst_notok are pure shell, so the # loop below spawns nothing at all. _cmp=${TMPDIR:-/tmp}/bst-safec-cmp.$$ awk ' NR==FNR { # pass 1: the probe output if ($1 != "" && $1 != "PROBE-END") { k=$1; $1=""; sub(/^ /,""); got[k]=$0; seen[k]=1 } next } { # pass 2: the native control if ($1 == "" || $1 == "PROBE-END") next k=$1; $1=""; sub(/^ /,""); want=$0 if (seen[k]) printf "%s\t%s\t%s\t%s\n", (got[k] == want ? "OK" : "DIFF"), k, want, got[k] else printf "MISS\t%s\t%s\t\n", k, want } ' "$pout" "$NATIVE" > "$_cmp" _tab=$(printf '\t') while IFS="$_tab" read -r verdict id want got; do [ -n "$id" ] || continue case "$verdict" in OK) bst_ok "safec/$id" ;; DIFF) bst_notok "safec/$id" "want:'$want' got:'$got'" ;; MISS) bst_notok "safec/$id" "no value on target (probe stopped before this case); native='$want'" ;; esac done < "$_cmp" rm -f "$pout" "$_cmp"