From c0544f9daaa4c2da253690db7bfc1c0c96c1d8c5 Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Mon, 16 Feb 2026 15:54:47 +0100 Subject: [PATCH 1/3] Escape process parameters --- Cargo.lock | 1 + Cargo.toml | 1 + fact/Cargo.toml | 1 + fact/src/event/process.rs | 8 ++++---- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 085648b8..a67f03ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -457,6 +457,7 @@ dependencies = [ "regex", "serde", "serde_json", + "shlex", "tempfile", "tokio", "tokio-native-tls", diff --git a/Cargo.toml b/Cargo.toml index 742c25f4..45b275fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ prost = "0.14.0" prost-types = "0.14.0" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.142" +shlex = "1.3.0" tokio = { version = "1.40.0", default-features = false, features = [ "fs", "macros", diff --git a/fact/Cargo.toml b/fact/Cargo.toml index 3b84db24..9b1b52e3 100644 --- a/fact/Cargo.toml +++ b/fact/Cargo.toml @@ -27,6 +27,7 @@ prost = { workspace = true } prost-types = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +shlex = { workspace = true } uuid = { workspace = true } yaml-rust2 = { workspace = true } diff --git a/fact/src/event/process.rs b/fact/src/event/process.rs index 5e42e530..5390f805 100644 --- a/fact/src/event/process.rs +++ b/fact/src/event/process.rs @@ -192,10 +192,10 @@ impl From for fact_api::ProcessSignal { let container_id = container_id.unwrap_or("".to_string()); - let args = args - .into_iter() - .reduce(|acc, i| acc + " " + &i) - .unwrap_or("".to_owned()); + // try_join can fail if args contain nul bytes, though this should not happen + // since args are parsed from C strings which are nul-terminated + let args = shlex::try_join(args.iter().map(|s| s.as_str())) + .unwrap_or_else(|_| String::new()); Self { id: Uuid::new_v4().to_string(), From c2765d8899180d9869c9bb56d9e4918afd0755ec Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Mon, 16 Feb 2026 15:57:32 +0100 Subject: [PATCH 2/3] Update tests to consider quoting --- tests/event.py | 16 ++++++++++++++- tests/test_editors/test_nvim.py | 24 +++++++++++----------- tests/test_editors/test_sed.py | 20 ++++++++++-------- tests/test_editors/test_vi.py | 36 ++++++++++++++++++--------------- tests/test_editors/test_vim.py | 28 ++++++++++++++----------- 5 files changed, 75 insertions(+), 49 deletions(-) diff --git a/tests/event.py b/tests/event.py index 7fee2cb8..31105174 100644 --- a/tests/event.py +++ b/tests/event.py @@ -1,4 +1,5 @@ import os +import re from re import Pattern import string from enum import Enum @@ -25,6 +26,19 @@ def extract_container_id(cgroup: str) -> str: else: return '' +def rust_style_quote(s): + if not s: + return "''" + if re.search(r'[^a-zA-Z0-9_./-]', s): + # Try to match the behavior of shlex.try_join() + if '\'' in s and not '"' in s: + return f'"{s}"' + escaped = s.replace("'", "\\'") + return f"'{escaped}'" + return s + +def rust_style_join(args): + return ' '.join(rust_style_quote(arg) for arg in args) class EventType(Enum): """Enumeration for different types of file activity events.""" @@ -85,7 +99,7 @@ def get_id(line: str, wanted_id: str) -> int | None: content = f.read(4096) args = [arg.decode('utf-8') for arg in content.split(b'\x00') if arg] - args = ' '.join(args) + args = rust_style_join(args) with open(os.path.join(proc_dir, 'comm'), 'r') as f: name = f.read().strip() diff --git a/tests/test_editors/test_nvim.py b/tests/test_editors/test_nvim.py index 32175d42..2059a431 100644 --- a/tests/test_editors/test_nvim.py +++ b/tests/test_editors/test_nvim.py @@ -5,13 +5,13 @@ def test_new_file(editor_container, server): fut = '/mounted/test.txt' + cmd = f"nvim {fut} '+:normal iThis is a test' -c x" - editor_container.exec_run( - f"nvim {fut} +':normal iThis is a test' -c x") + editor_container.exec_run(cmd) process = Process.in_container( exe_path='/usr/bin/nvim', - args=f'nvim {fut} +:normal iThis is a test -c x', + args=cmd, name='nvim', container_id=editor_container.id[:12], ) @@ -25,12 +25,12 @@ def test_new_file(editor_container, server): def test_open_file(editor_container, server): fut = '/mounted/test.txt' + cmd = f"nvim {fut} '+:normal iThis is a test' -c x" container_id = editor_container.id[:12] # We ensure the file exists before editing. editor_container.exec_run(f'touch {fut}') - editor_container.exec_run( - f"nvim {fut} +':normal iThis is a test' -c x") + editor_container.exec_run(cmd) touch = Process.in_container( exe_path='/usr/bin/touch', @@ -40,7 +40,7 @@ def test_open_file(editor_container, server): ) nvim = Process.in_container( exe_path='/usr/bin/nvim', - args=f'nvim {fut} +:normal iThis is a test -c x', + args=cmd, name='nvim', container_id=container_id, ) @@ -69,13 +69,13 @@ def test_open_file(editor_container, server): def test_new_file_ovfs(editor_container, server): fut = '/container-dir/test.txt' + cmd = f"nvim {fut} '+:normal iThis is a test' -c x" - editor_container.exec_run( - f"nvim {fut} +':normal iThis is a test' -c x") + editor_container.exec_run(cmd) process = Process.in_container( exe_path='/usr/bin/nvim', - args=f'nvim {fut} +:normal iThis is a test -c x', + args=cmd, name='nvim', container_id=editor_container.id[:12], ) @@ -91,12 +91,12 @@ def test_new_file_ovfs(editor_container, server): def test_open_file_ovfs(editor_container, server): fut = '/container-dir/test.txt' + cmd = f"nvim {fut} '+:normal iThis is a test' -c x" container_id = editor_container.id[:12] # We ensure the file exists before editing. editor_container.exec_run(f'touch {fut}') - editor_container.exec_run( - f"nvim {fut} +':normal iThis is a test' -c x") + editor_container.exec_run(cmd) touch = Process.in_container( exe_path='/usr/bin/touch', @@ -106,7 +106,7 @@ def test_open_file_ovfs(editor_container, server): ) nvim = Process.in_container( exe_path='/usr/bin/nvim', - args=f'nvim {fut} +:normal iThis is a test -c x', + args=cmd, name='nvim', container_id=container_id, ) diff --git a/tests/test_editors/test_sed.py b/tests/test_editors/test_sed.py index 99accb88..826df6c9 100644 --- a/tests/test_editors/test_sed.py +++ b/tests/test_editors/test_sed.py @@ -5,20 +5,22 @@ def test_sed(vi_container, server): # File Under Test fut = '/mounted/test.txt' + create_cmd = f"sh -c \"echo 'This is a test' > {fut}\"" + sed_cmd = fr'sed -i -e "s/a test/not \\0/" {fut}' container_id = vi_container.id[:12] - vi_container.exec_run(f"sh -c \"echo 'This is a test' > {fut}\"") - vi_container.exec_run(fr"sed -i -e 's/a test/not \0/' {fut}") + vi_container.exec_run(create_cmd) + vi_container.exec_run(sed_cmd) shell = Process.in_container( exe_path='/usr/bin/bash', - args=f"sh -c echo 'This is a test' > {fut}", + args=create_cmd, name='sh', container_id=container_id, ) sed = Process.in_container( exe_path='/usr/bin/sed', - args=fr'sed -i -e s/a test/not \0/ {fut}', + args=sed_cmd, name='sed', container_id=container_id, ) @@ -40,20 +42,22 @@ def test_sed(vi_container, server): def test_sed_ovfs(vi_container, server): # File Under Test fut = '/container-dir/test.txt' + create_cmd = f"sh -c \"echo 'This is a test' > {fut}\"" + sed_cmd = fr'sed -i -e "s/a test/not \\0/" {fut}' container_id = vi_container.id[:12] - vi_container.exec_run(f"sh -c \"echo 'This is a test' > {fut}\"") - vi_container.exec_run(fr"sed -i -e 's/a test/not \0/' {fut}") + vi_container.exec_run(create_cmd) + vi_container.exec_run(sed_cmd) shell = Process.in_container( exe_path='/usr/bin/bash', - args=f"sh -c echo 'This is a test' > {fut}", + args=create_cmd, name='sh', container_id=container_id, ) sed = Process.in_container( exe_path='/usr/bin/sed', - args=fr'sed -i -e s/a test/not \0/ {fut}', + args=sed_cmd, name='sed', container_id=container_id, ) diff --git a/tests/test_editors/test_vi.py b/tests/test_editors/test_vi.py index 65c4ef06..d6dad97e 100644 --- a/tests/test_editors/test_vi.py +++ b/tests/test_editors/test_vi.py @@ -6,14 +6,15 @@ def test_new_file(vi_container, server): fut = '/mounted/test.txt' swap_file = '/mounted/.test.txt.swp' swx_file = '/mounted/.test.txt.swx' - exe = '/usr/bin/vi' + exe = '/usr/libexec/vi' - vi_container.exec_run( - f"vi {fut} +':normal iThis is a test' -c x") + cmd = f"{exe} {fut} '+:normal iThis is a test' -c x" + + vi_container.exec_run(cmd) process = Process.in_container( exe_path=exe, - args=f'vi {fut} +:normal iThis is a test -c x', + args=cmd, name='vi', container_id=vi_container.id[:12], ) @@ -42,14 +43,15 @@ def test_new_file_ovfs(vi_container, server): fut = '/container-dir/test.txt' swap_file = '/container-dir/.test.txt.swp' swx_file = '/container-dir/.test.txt.swx' - exe = '/usr/bin/vi' + exe = '/usr/libexec/vi' + + cmd = f"{exe} {fut} '+:normal iThis is a test' -c x" - vi_container.exec_run( - f"vi {fut} +':normal iThis is a test' -c x") + vi_container.exec_run(cmd) process = Process.in_container( exe_path=exe, - args=f'vi {fut} +:normal iThis is a test -c x', + args=cmd, name='vi', container_id=vi_container.id[:12], ) @@ -87,13 +89,14 @@ def test_open_file(vi_container, server): swap_file = '/mounted/.test.txt.swp' swx_file = '/mounted/.test.txt.swx' vi_test_file = get_vi_test_file('/mounted') - exe = '/usr/bin/vi' + exe = '/usr/libexec/vi' container_id = vi_container.id[:12] + cmd = f"{exe} {fut} '+:normal iThis is a test' -c x" + # We ensure the file exists before editing. vi_container.exec_run(f'touch {fut}') - vi_container.exec_run( - f"vi {fut} +':normal iThis is a test' -c x") + vi_container.exec_run(cmd) touch_process = Process.in_container( exe_path='/usr/bin/touch', @@ -103,7 +106,7 @@ def test_open_file(vi_container, server): ) vi_process = Process.in_container( exe_path=exe, - args=f'vi {fut} +:normal iThis is a test -c x', + args=cmd, name='vi', container_id=container_id, ) @@ -147,13 +150,14 @@ def test_open_file_ovfs(vi_container, server): swap_file = '/container-dir/.test.txt.swp' swx_file = '/container-dir/.test.txt.swx' vi_test_file = get_vi_test_file('/container-dir') - exe = '/usr/bin/vi' + exe = '/usr/libexec/vi' container_id = vi_container.id[:12] + cmd = f"{exe} {fut} '+:normal iThis is a test' -c x" + # We ensure the file exists before editing. vi_container.exec_run(f'touch {fut}') - vi_container.exec_run( - f"vi {fut} +':normal iThis is a test' -c x") + vi_container.exec_run(cmd) touch_process = Process.in_container( exe_path='/usr/bin/touch', @@ -163,7 +167,7 @@ def test_open_file_ovfs(vi_container, server): ) vi_process = Process.in_container( exe_path=exe, - args=f'vi {fut} +:normal iThis is a test -c x', + args=cmd, name='vi', container_id=container_id, ) diff --git a/tests/test_editors/test_vim.py b/tests/test_editors/test_vim.py index 8919362b..f8987eee 100644 --- a/tests/test_editors/test_vim.py +++ b/tests/test_editors/test_vim.py @@ -7,12 +7,13 @@ def test_new_file(editor_container, server): swap_file = '/mounted/.test.txt.swp' swx_file = '/mounted/.test.txt.swx' - editor_container.exec_run( - f"vim {fut} +':normal iThis is a test' -c x") + cmd = f"vim {fut} '+:normal iThis is a test' -c x" + + editor_container.exec_run(cmd) process = Process.in_container( exe_path='/usr/bin/vim', - args=f'vim {fut} +:normal iThis is a test -c x', + args=cmd, name='vim', container_id=editor_container.id[:12], ) @@ -41,12 +42,13 @@ def test_new_file_ovfs(editor_container, server): swap_file = '/container-dir/.test.txt.swp' swx_file = '/container-dir/.test.txt.swx' - editor_container.exec_run( - f"vim {fut} +':normal iThis is a test' -c x") + cmd = f"vim {fut} '+:normal iThis is a test' -c x" + + editor_container.exec_run(cmd) process = Process.in_container( exe_path='/usr/bin/vim', - args=f'vim {fut} +:normal iThis is a test -c x', + args=cmd, name='vim', container_id=editor_container.id[:12], ) @@ -85,10 +87,11 @@ def test_open_file(editor_container, server): vi_test_file = get_vi_test_file('/mounted') container_id = editor_container.id[:12] + cmd = f"vim {fut} '+:normal iThis is a test' -c x" + # We ensure the file exists before editing. editor_container.exec_run(f'touch {fut}') - editor_container.exec_run( - f"vim {fut} +':normal iThis is a test' -c x") + editor_container.exec_run(cmd) touch_process = Process.in_container( exe_path='/usr/bin/touch', @@ -98,7 +101,7 @@ def test_open_file(editor_container, server): ) vi_process = Process.in_container( exe_path='/usr/bin/vim', - args=f'vim {fut} +:normal iThis is a test -c x', + args=cmd, name='vim', container_id=container_id, ) @@ -144,10 +147,11 @@ def test_open_file_ovfs(editor_container, server): vi_test_file = get_vi_test_file('/container-dir') container_id = editor_container.id[:12] + cmd = f"vim {fut} '+:normal iThis is a test' -c x" + # We ensure the file exists before editing. editor_container.exec_run(f'touch {fut}') - editor_container.exec_run( - f"vim {fut} +':normal iThis is a test' -c x") + editor_container.exec_run(cmd) touch_process = Process.in_container( exe_path='/usr/bin/touch', @@ -157,7 +161,7 @@ def test_open_file_ovfs(editor_container, server): ) vi_process = Process.in_container( exe_path='/usr/bin/vim', - args=f'vim {fut} +:normal iThis is a test -c x', + args=cmd, name='vim', container_id=container_id, ) From bb74105c72582aaa595720ab340ee6fb53c45362 Mon Sep 17 00:00:00 2001 From: Olivier Valentin Date: Mon, 16 Feb 2026 16:06:10 +0100 Subject: [PATCH 3/3] Formatting --- fact/src/event/process.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fact/src/event/process.rs b/fact/src/event/process.rs index 5390f805..a601eeab 100644 --- a/fact/src/event/process.rs +++ b/fact/src/event/process.rs @@ -194,8 +194,8 @@ impl From for fact_api::ProcessSignal { // try_join can fail if args contain nul bytes, though this should not happen // since args are parsed from C strings which are nul-terminated - let args = shlex::try_join(args.iter().map(|s| s.as_str())) - .unwrap_or_else(|_| String::new()); + let args = + shlex::try_join(args.iter().map(|s| s.as_str())).unwrap_or_else(|_| String::new()); Self { id: Uuid::new_v4().to_string(),