Describe the bug
UAC uses sed 's|.|\&|g' to escape filesystem paths before piping them to xargs. xargs on Busybox (at least some versions) don't interpret backslash escape sequences correctly. This causes artifacts that rely on this behavior (such as bodyfile) to generate extremely long (300k+ character) error lines as the log file fills with stat: can't stat '\/\e\t\c\/\p\a\s\s\w\d': No such file or directory.
To Reproduce
This was observed on BusyBox v1.22.1. I'm not sure exactly how to reproduce it in another environment except to say that you need an xargs version that interprets \x as a literal \x and not x
Expected behavior
UAC detects the xargs limitations and falls back to another method
UAC Information
Target System Information:
- OS: Yocto Linux
- Version: kernel 3.x, BusyBox v1.22.1
- Arch: ARMv6b
Additional context
The following is an AI generated description of the issue and the solution that worked for me. I want to stress that this isn't an "AI found" hypothetical bug, I did observe the behavior and the solution AI provided seems to work, though I'm not sure it's the proper or correct solution. It probably also does a much better job at explaining the issue than I can.
BusyBox Compatibility: sed | xargs Path Escaping Fails on BusyBox Systems
Affects: UAC 3.3.0
Platform: BusyBox ash (embedded Linux, e.g. IoT devices, gateways, routers, etc.)
Collectors affected: stat, hash, find (with command)
Symptom: Bodyfile and hash outputs are not created; no error is logged
Problem
UAC uses sed 's|.|\\&|g' to escape filesystem paths before piping them to xargs. This works correctly on GNU/Linux systems because GNU xargs interprets \x as a literal x, stripping the backslashes before passing paths to the target tool.
BusyBox xargs does not interpret backslash escape sequences. It passes the literal backslash-prefixed string directly to the target tool (e.g. stat, md5sum). This causes every invocation to fail with:
stat: can't stat '\/\e\t\c\/\p\a\s\s\w\d': No such file or directory
This occurs in the fallback code path taken when xargs -0 is not supported (i.e. when __UAC_TOOL_XARGS_NULL_DELIMITER_SUPPORT=false), which is the common case on BusyBox-based embedded devices. The is_file_list branches use the same sed | xargs pattern unconditionally, regardless of xargs capabilities.
The result is that all stat and hash collectors produce no output, the output file is empty, it is silently deleted, and no bodyfile or hash artifacts appear in the collection archive.
Root Cause
In lib/find_based_collector.sh, the fallback path (taken when xargs -0 is unavailable) uses sed 's|.|\\\\&|g' | xargs to pass paths to stat/hash/command tools. In lib/remove_non_regular_files.sh, the same pattern is used unconditionally.
On GNU xargs, the backslash sequences are stripped before the path is passed to the subprocess. On BusyBox xargs, they are not — the tool receives \/\e\t\c\/\p\a\s\s\w\d as the literal path argument, which does not exist.
Solution
Add a capability probe that detects whether the system's xargs interprets backslash escape sequences. The probe feeds a backslash-escaped path to xargs and checks whether the backslash is stripped:
# GNU xargs: echo '\/tmp' | xargs echo → /tmp (backslash stripped)
# BusyBox: echo '\/tmp' | xargs echo → \/tmp (passed literally)
if [ "$(echo '\/tmp' | xargs echo 2>/dev/null)" = "/tmp" ]; then
__UAC_TOOL_XARGS_BACKSLASH_ESCAPE_SUPPORT=true
fi
Each affected code path then branches on this flag: the original sed | xargs method is used when the probe passes, and a while IFS= read -r loop is used as the fallback when it does not:
if ${__UAC_TOOL_XARGS_BACKSLASH_ESCAPE_SUPPORT}; then
__fc_stat_command="${__fc_find_command} | sed 's|.|\\\\&|g' | xargs ${__UAC_TOOL_STAT_BIN} ..."
else
__fc_stat_command="${__fc_find_command} | while IFS= read -r __fc_line; do ${__UAC_TOOL_STAT_BIN} ... \"\${__fc_line}\"; done"
fi
The while IFS= read -r approach:
- Calls the tool once per path (correct on all platforms)
- Handles filenames containing spaces, brackets, and other special characters
- Is fully POSIX sh / BusyBox ash compatible
- Requires no xargs at all
The find -print0 | xargs -0 path (taken when both are supported) is unchanged. On standard GNU/Linux systems the new probe passes and the original sed | xargs path is taken — no behaviour change on those systems.
Changes
lib/setup_tools.sh — probe added alongside existing xargs probe:
- Initialize
__UAC_TOOL_XARGS_BACKSLASH_ESCAPE_SUPPORT=false
- Add probe:
[ "$(echo '\/tmp' | xargs echo 2>/dev/null)" = "/tmp" ]
uac — log line added alongside existing xargs log line:
xargs backslash escape support: ${__UAC_TOOL_XARGS_BACKSLASH_ESCAPE_SUPPORT}
lib/find_based_collector.sh — 6 locations:
| Collector |
Branch |
GNU xargs (probe=true) |
BusyBox fallback (probe=false) |
find (with command) |
is_file_list |
sed | xargs cmd |
while read; do cmd "$line"; done < file |
find (with command) |
else (no xargs-0) |
find | sed | xargs cmd |
find | while read; do cmd "$line"; done |
hash |
is_file_list |
sed | xargs hash |
while read; do hash "$line"; done < file |
hash |
else (no xargs-0) |
find | sed | xargs hash |
find | while read; do hash "$line"; done |
stat |
is_file_list |
sed | xargs stat |
while read; do stat "$line"; done < file |
stat |
else (no xargs-0) |
find | sed | xargs stat |
find | while read; do stat "$line"; done |
lib/remove_non_regular_files.sh — 1 location, same conditional pattern.
Verification
Tested on BusyBox device (Linux, ash). The following command replicates the fixed stat collector pipeline for a single file:
echo "/etc/passwd" | while IFS= read -r __fc_line; do stat -c "0|%N|%i|%A|%u|%g|%s|%X|%Y|%Z|%W" "${__fc_line}"; done | sed -e "s:|':|:g" -e "s:'|:|:g" -e "s:' -> ': -> :" -e 's:|":|:g' -e 's:"|:|:g' -e 's:" -> ": -> :' -e "s:\`::g" -e "s:|.\{1,4\}$:|0:"
Expected output:
0|/etc/passwd|69|-rw-r--r--|0|0|945|1661229428|1661229428|1661229428|0
Note: BusyBox stat %W outputs the literal character W (birth time unsupported). The existing sed rule s:|.\{1,4\}$:|0: correctly converts this to 0. BusyBox sed was confirmed to support \{n,m\} interval expressions.
A full UAC run on the BusyBox device produced a valid bodyfile. A separate test on a standard Linux system confirmed the probe correctly selects the sed | xargs path, with no change in behaviour on that platform.
Diff
diff --git a/lib/setup_tools.sh b/lib/setup_tools.sh
--- a/lib/setup_tools.sh
+++ b/lib/setup_tools.sh
@@ -24,6 +24,7 @@
__UAC_TOOL_FIND_PRINT0_SUPPORT=false
__UAC_TOOL_XARGS_NULL_DELIMITER_SUPPORT=false
+ __UAC_TOOL_XARGS_BACKSLASH_ESCAPE_SUPPORT=false
@@ -75,6 +76,9 @@
if echo "uac" | xargs -0 echo >/dev/null; then
__UAC_TOOL_XARGS_NULL_DELIMITER_SUPPORT=true
fi
+ if [ "$(echo '\/tmp' | xargs echo 2>/dev/null)" = "/tmp" ]; then
+ __UAC_TOOL_XARGS_BACKSLASH_ESCAPE_SUPPORT=true
+ fi
diff --git a/uac b/uac
--- a/uac
+++ b/uac
@@ -458,6 +458,7 @@
_log_msg INF "xargs -0 support: ${__UAC_TOOL_XARGS_NULL_DELIMITER_SUPPORT}"
+_log_msg INF "xargs backslash escape support: ${__UAC_TOOL_XARGS_BACKSLASH_ESCAPE_SUPPORT}"
diff --git a/lib/find_based_collector.sh b/lib/find_based_collector.sh
--- a/lib/find_based_collector.sh
+++ b/lib/find_based_collector.sh
@@ find collector, is_file_list branch @@
if ${__fc_is_file_list}; then
- __fc_command="sed 's|.|\\\\&|g' \"${__fc_path}\" | xargs ${__fc_command}"
+ if ${__UAC_TOOL_XARGS_BACKSLASH_ESCAPE_SUPPORT}; then
+ __fc_command="sed 's|.|\\\\&|g' \"${__fc_path}\" | xargs ${__fc_command}"
+ else
+ __fc_command="while IFS= read -r __fc_line; do ${__fc_command} \"\${__fc_line}\"; done <\"${__fc_path}\""
+ fi
@@ find collector, else branch (no xargs-0) @@
- __fc_command="${__fc_find_command} | sed 's|.|\\\\&|g' | xargs ${__fc_command}"
+ if ${__UAC_TOOL_XARGS_BACKSLASH_ESCAPE_SUPPORT}; then
+ __fc_command="${__fc_find_command} | sed 's|.|\\\\&|g' | xargs ${__fc_command}"
+ else
+ __fc_command="${__fc_find_command} | while IFS= read -r __fc_line; do ${__fc_command} \"\${__fc_line}\"; done"
+ fi
@@ hash collector, is_file_list branch @@
if ${__fc_is_file_list}; then
- __fc_hash_command="sed 's|.|\\\\&|g' \"${__fc_path}\" | xargs ${__fc_hashing_tool}"
+ if ${__UAC_TOOL_XARGS_BACKSLASH_ESCAPE_SUPPORT}; then
+ __fc_hash_command="sed 's|.|\\\\&|g' \"${__fc_path}\" | xargs ${__fc_hashing_tool}"
+ else
+ __fc_hash_command="while IFS= read -r __fc_line; do ${__fc_hashing_tool} \"\${__fc_line}\"; done <\"${__fc_path}\""
+ fi
@@ hash collector, else branch (no xargs-0) @@
- __fc_hash_command="${__fc_find_command} | sed 's|.|\\\\&|g' | xargs ${__fc_hashing_tool}"
+ if ${__UAC_TOOL_XARGS_BACKSLASH_ESCAPE_SUPPORT}; then
+ __fc_hash_command="${__fc_find_command} | sed 's|.|\\\\&|g' | xargs ${__fc_hashing_tool}"
+ else
+ __fc_hash_command="${__fc_find_command} | while IFS= read -r __fc_line; do ${__fc_hashing_tool} \"\${__fc_line}\"; done"
+ fi
@@ stat collector, is_file_list branch @@
if ${__fc_is_file_list}; then
- __fc_stat_command="sed 's|.|\\\\&|g' \"${__fc_path}\" | xargs ${__UAC_TOOL_STAT_BIN}${__UAC_TOOL_STAT_PARAMS:+ }${__UAC_TOOL_STAT_PARAMS}"
+ if ${__UAC_TOOL_XARGS_BACKSLASH_ESCAPE_SUPPORT}; then
+ __fc_stat_command="sed 's|.|\\\\&|g' \"${__fc_path}\" | xargs ${__UAC_TOOL_STAT_BIN}${__UAC_TOOL_STAT_PARAMS:+ }${__UAC_TOOL_STAT_PARAMS}"
+ else
+ __fc_stat_command="while IFS= read -r __fc_line; do ${__UAC_TOOL_STAT_BIN}${__UAC_TOOL_STAT_PARAMS:+ }${__UAC_TOOL_STAT_PARAMS} \"\${__fc_line}\"; done <\"${__fc_path}\""
+ fi
@@ stat collector, else branch (no xargs-0) @@
- __fc_stat_command="${__fc_find_command} | sed 's|.|\\\\&|g' | xargs ${__UAC_TOOL_STAT_BIN}${__UAC_TOOL_STAT_PARAMS:+ }${__UAC_TOOL_STAT_PARAMS}"
+ if ${__UAC_TOOL_XARGS_BACKSLASH_ESCAPE_SUPPORT}; then
+ __fc_stat_command="${__fc_find_command} | sed 's|.|\\\\&|g' | xargs ${__UAC_TOOL_STAT_BIN}${__UAC_TOOL_STAT_PARAMS:+ }${__UAC_TOOL_STAT_PARAMS}"
+ else
+ __fc_stat_command="${__fc_find_command} | while IFS= read -r __fc_line; do ${__UAC_TOOL_STAT_BIN}${__UAC_TOOL_STAT_PARAMS:+ }${__UAC_TOOL_STAT_PARAMS} \"\${__fc_line}\"; done"
+ fi
diff --git a/lib/remove_non_regular_files.sh b/lib/remove_non_regular_files.sh
--- a/lib/remove_non_regular_files.sh
+++ b/lib/remove_non_regular_files.sh
@@ -20,7 +20,11 @@ _remove_non_regular_files()
fi
- __rn_command="sed 's|.|\\\\&|g' \"${__rn_file}\" | xargs find"
+ if ${__UAC_TOOL_XARGS_BACKSLASH_ESCAPE_SUPPORT}; then
+ __rn_command="sed 's|.|\\\\&|g' \"${__rn_file}\" | xargs find"
+ else
+ __rn_command="while IFS= read -r __rn_line; do find \"\${__rn_line}\" -maxdepth 0 2>/dev/null; done <\"${__rn_file}\""
+ fi
Describe the bug
UAC uses sed 's|.|\&|g' to escape filesystem paths before piping them to xargs. xargs on Busybox (at least some versions) don't interpret backslash escape sequences correctly. This causes artifacts that rely on this behavior (such as bodyfile) to generate extremely long (300k+ character) error lines as the log file fills with
stat: can't stat '\/\e\t\c\/\p\a\s\s\w\d': No such file or directory.To Reproduce
This was observed on BusyBox v1.22.1. I'm not sure exactly how to reproduce it in another environment except to say that you need an xargs version that interprets \x as a literal \x and not x
Expected behavior
UAC detects the xargs limitations and falls back to another method
UAC Information
Target System Information:
Additional context
The following is an AI generated description of the issue and the solution that worked for me. I want to stress that this isn't an "AI found" hypothetical bug, I did observe the behavior and the solution AI provided seems to work, though I'm not sure it's the proper or correct solution. It probably also does a much better job at explaining the issue than I can.
BusyBox Compatibility:
sed | xargsPath Escaping Fails on BusyBox SystemsAffects: UAC 3.3.0
Platform: BusyBox ash (embedded Linux, e.g. IoT devices, gateways, routers, etc.)
Collectors affected:
stat,hash,find(with command)Symptom: Bodyfile and hash outputs are not created; no error is logged
Problem
UAC uses
sed 's|.|\\&|g'to escape filesystem paths before piping them toxargs. This works correctly on GNU/Linux systems because GNU xargs interprets\xas a literalx, stripping the backslashes before passing paths to the target tool.BusyBox xargs does not interpret backslash escape sequences. It passes the literal backslash-prefixed string directly to the target tool (e.g.
stat,md5sum). This causes every invocation to fail with:This occurs in the fallback code path taken when
xargs -0is not supported (i.e. when__UAC_TOOL_XARGS_NULL_DELIMITER_SUPPORT=false), which is the common case on BusyBox-based embedded devices. Theis_file_listbranches use the samesed | xargspattern unconditionally, regardless of xargs capabilities.The result is that all stat and hash collectors produce no output, the output file is empty, it is silently deleted, and no bodyfile or hash artifacts appear in the collection archive.
Root Cause
In
lib/find_based_collector.sh, the fallback path (taken whenxargs -0is unavailable) usessed 's|.|\\\\&|g' | xargsto pass paths to stat/hash/command tools. Inlib/remove_non_regular_files.sh, the same pattern is used unconditionally.On GNU xargs, the backslash sequences are stripped before the path is passed to the subprocess. On BusyBox xargs, they are not — the tool receives
\/\e\t\c\/\p\a\s\s\w\das the literal path argument, which does not exist.Solution
Add a capability probe that detects whether the system's xargs interprets backslash escape sequences. The probe feeds a backslash-escaped path to xargs and checks whether the backslash is stripped:
Each affected code path then branches on this flag: the original
sed | xargsmethod is used when the probe passes, and awhile IFS= read -rloop is used as the fallback when it does not:The
while IFS= read -rapproach:The
find -print0 | xargs -0path (taken when both are supported) is unchanged. On standard GNU/Linux systems the new probe passes and the originalsed | xargspath is taken — no behaviour change on those systems.Changes
lib/setup_tools.sh— probe added alongside existing xargs probe:__UAC_TOOL_XARGS_BACKSLASH_ESCAPE_SUPPORT=false[ "$(echo '\/tmp' | xargs echo 2>/dev/null)" = "/tmp" ]uac— log line added alongside existing xargs log line:xargs backslash escape support: ${__UAC_TOOL_XARGS_BACKSLASH_ESCAPE_SUPPORT}lib/find_based_collector.sh— 6 locations:find(with command)is_file_listsed | xargs cmdwhile read; do cmd "$line"; done < filefind(with command)else(no xargs-0)find | sed | xargs cmdfind | while read; do cmd "$line"; donehashis_file_listsed | xargs hashwhile read; do hash "$line"; done < filehashelse(no xargs-0)find | sed | xargs hashfind | while read; do hash "$line"; donestatis_file_listsed | xargs statwhile read; do stat "$line"; done < filestatelse(no xargs-0)find | sed | xargs statfind | while read; do stat "$line"; donelib/remove_non_regular_files.sh— 1 location, same conditional pattern.Verification
Tested on BusyBox device (Linux, ash). The following command replicates the fixed stat collector pipeline for a single file:
Expected output:
Note: BusyBox
stat %Woutputs the literal characterW(birth time unsupported). The existing sed rules:|.\{1,4\}$:|0:correctly converts this to0. BusyBox sed was confirmed to support\{n,m\}interval expressions.A full UAC run on the BusyBox device produced a valid bodyfile. A separate test on a standard Linux system confirmed the probe correctly selects the
sed | xargspath, with no change in behaviour on that platform.Diff