Skip to content

Commit

Permalink
Limit how many bytes are copied from the command line.
Browse files Browse the repository at this point in the history
The new option `TIMEHISTORY_CMDLINE_LIMIT` (512 by default) can be used to limit
how many bytes are copied from the command line to the history entry.

If a command line exceeds this limit, the arguments are truncated.

The size of a CString written to the shared buffer is written as a `usize`
instead of `u16`.
  • Loading branch information
ayosec committed Oct 29, 2021
1 parent f88b4b9 commit 82a1a74
Show file tree
Hide file tree
Showing 10 changed files with 124 additions and 29 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,13 @@ timehistory configuration can be modified using shell variables:
When an entry is added to the history, and the number of entries exceeds
this limit, the oldest entry is removed.

* `TIMEHISTORY_CMDLINE_LIMIT`

Set the maximum number of bytes from the command line to be added to the
history.

If a command line exceeds this limit, then it is truncated.

The current configuration settings are printed with `timehistory -s`:

```console
Expand Down
8 changes: 5 additions & 3 deletions src/ipc/events/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ impl ExecEvent {
start_time: libc::timespec,
filename: *const libc::c_char,
argv: *const *const libc::c_char,
mut max_cmdline: usize,
) -> io::Result<usize>
where
T: Write + Seek,
Expand All @@ -49,11 +50,12 @@ impl ExecEvent {
output.write_value(&start_time)?;

// filename and argv fields.
output.write_cstr(filename)?;
output.write_cstr(filename, max_cmdline)?;

let mut arg = argv;
while !(*arg).is_null() {
output.write_cstr(*arg)?;
while max_cmdline > 0 && !(*arg).is_null() {
let written = output.write_cstr(*arg, max_cmdline)?;
max_cmdline = max_cmdline.saturating_sub(written);
arg = arg.add(1);
}

Expand Down
24 changes: 10 additions & 14 deletions src/ipc/events/ioext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ pub(super) trait WriteExt {

/// Write a C string to `output`.
///
/// The size is written in 2 bytes before the string, and it is
/// limited to `u16::MAX`.
unsafe fn write_cstr(&mut self, ptr: *const libc::c_char) -> io::Result<()>;
/// The size is written as a `usize` before the string, and it is limited to
/// `limit`.
///
/// Returns how many bytes are written.
unsafe fn write_cstr(&mut self, ptr: *const libc::c_char, limit: usize) -> io::Result<usize>;
}

impl<R: Read> ReadExt for R {
Expand All @@ -37,7 +39,7 @@ impl<R: Read> ReadExt for R {
}

fn read_cstr(&mut self) -> io::Result<OsString> {
let size = unsafe { self.read_value::<u16>()? as usize };
let size = unsafe { self.read_value::<usize>()? };
let mut bytes = vec![0; size];
self.read_exact(&mut bytes)?;
Ok(OsString::from_vec(bytes))
Expand All @@ -51,23 +53,17 @@ impl<W: Write> WriteExt for W {
self.write_all(slice)
}

unsafe fn write_cstr(&mut self, ptr: *const libc::c_char) -> io::Result<()> {
let end: *const libc::c_char = libc::memchr(ptr.cast(), 0, u16::MAX as usize).cast();

unsafe fn write_cstr(&mut self, ptr: *const libc::c_char, limit: usize) -> io::Result<usize> {
// String size.
let size = if end.is_null() {
u16::MAX
} else {
end.offset_from(ptr) as u16
};
let size = libc::strnlen(ptr.cast(), limit);

self.write_value(&size)?;

// String bytes.
let slice = std::slice::from_raw_parts(ptr.cast(), size as usize);
let slice = std::slice::from_raw_parts(ptr.cast(), size);
self.write_all(slice)?;

Ok(())
Ok(size)
}
}

Expand Down
7 changes: 5 additions & 2 deletions src/ipc/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ mod tests {
use super::*;
use std::ffi::OsString;
use std::io::Cursor;
use std::mem::discriminant;

macro_rules! cstr {
($s:literal) => {
Expand All @@ -155,7 +156,7 @@ mod tests {

#[test]
fn send_exec_events() {
let mut output = vec![0; 256];
let mut output = vec![0; 512];

// Send three events.

Expand All @@ -182,6 +183,7 @@ mod tests {
std::ptr::null(),
]
.as_ptr(),
usize::MAX,
)
.unwrap()
};
Expand All @@ -195,7 +197,8 @@ mod tests {
for idx in 0..3 {
let event = match events.next() {
Some(Event::Exec(e)) => e,
_ => panic!("invalid event"),
Some(e) => panic!("invalid event: {:?}", discriminant(&e)),
None => panic!("no more events"),
};

assert_eq!(event.pid, 1000 + idx as libc::pid_t);
Expand Down
35 changes: 34 additions & 1 deletion src/ipc/mod.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
pub mod events;
pub mod sharedbuffer;

use bash_builtins::error;
use std::ffi::{CStr, CString};
use std::io::{self, Write};
use std::mem::MaybeUninit;
use std::sync::Once;
use std::time::Duration;

use bash_builtins::{error, variables::DynamicVariable};

pub use sharedbuffer::{SharedBuffer, SharedBufferGuard};

/// Size for the shared buffer;
const SHARED_BUFFER_SIZE: usize = 16 * 1024;

/// Timeout to access the inner value of `max_cmdline` from the
/// `TIMEHISTORY_CMDLINE_LIMIT` variable.
const TIMEOUT_CMDLINE_VAR: Duration = Duration::from_secs(1);

/// Global reference to the shared buffer.
pub fn global_shared_buffer(timeout: Duration) -> Option<SharedBufferGuard<'static>> {
static mut BUFFER: MaybeUninit<Option<SharedBuffer>> = MaybeUninit::uninit();
Expand All @@ -34,3 +41,29 @@ pub fn global_shared_buffer(timeout: Duration) -> Option<SharedBufferGuard<'stat
let buffer = unsafe { (&*BUFFER.as_ptr()).as_ref() };
buffer.and_then(|b| b.lock(timeout).ok())
}

/// Dynamic variable to control the cmdline limit.
pub struct CmdLineLimitVariable;

impl DynamicVariable for CmdLineLimitVariable {
fn get(&mut self) -> std::option::Option<CString> {
let max_cmdline = global_shared_buffer(TIMEOUT_CMDLINE_VAR)?.max_cmdline();

CString::new(max_cmdline.to_string()).ok()
}

fn set(&mut self, value: &CStr) {
let max_cmdline = match value.to_str().map(str::parse) {
Ok(Ok(n)) => n,

_ => {
let _ = writeln!(io::stderr(), "timehistory: invalid number");
return;
}
};

if let Some(mut buffer) = global_shared_buffer(TIMEOUT_CMDLINE_VAR) {
buffer.set_max_cmdline(max_cmdline);
}
}
}
17 changes: 16 additions & 1 deletion src/ipc/sharedbuffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ use std::time::Duration;
/// Minimum size for the shared buffer.
const MIN_BUFFER_SIZE: usize = 4 * 1024;

/// Default value for `max_cmdline`.
const DEFAULT_MAX_CMDLINE: usize = 512;

/// Buffer that can be shared between multiple processes.
pub struct SharedBuffer {
buf: *mut libc::c_void,
Expand All @@ -23,6 +26,7 @@ pub struct SharedBuffer {
#[repr(C)]
struct SharedBufferHeader<const N: usize> {
mutex: UnsafeCell<libc::pthread_mutex_t>,
max_cmdline: usize,
cursor: usize,
data: [u8; N],
}
Expand Down Expand Up @@ -96,6 +100,7 @@ impl SharedBuffer {

// Data for the underlying buffer.
header.cursor = 0;
header.max_cmdline = DEFAULT_MAX_CMDLINE;
}

Ok(SharedBuffer { buf, len })
Expand Down Expand Up @@ -214,6 +219,16 @@ impl SharedBufferGuard<'_> {

unsafe { slice::from_raw_parts_mut(self.data_mut().add(cursor), len) }
}

/// Returns the maximum number of bytes to add in a single command line.
pub fn max_cmdline(&self) -> usize {
self.header().max_cmdline
}

/// Change the `max_cmdline` field.
pub fn set_max_cmdline(&mut self, value: usize) {
self.header_mut().max_cmdline = value;
}
}

impl Drop for SharedBufferGuard<'_> {
Expand All @@ -231,7 +246,7 @@ mod tests {
use std::sync::{Arc, Barrier};

const EXPECTED_HEADER_SIZE: usize =
mem::size_of::<libc::pthread_mutex_t>() + mem::size_of::<usize>();
mem::size_of::<libc::pthread_mutex_t>() + mem::size_of::<usize>() * 2;

#[test]
fn send_data() {
Expand Down
32 changes: 26 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ builtin_metadata!(
Settings:
The following shell variables can be used to change the configuration:
TIMEHISTORY_FORMAT\tDefault format string.
TIMEHISTORY_LIMIT\tHistory limit.
TIMEHISTORY_FORMAT Default format string.
TIMEHISTORY_LIMIT History limit.
TIMEHISTORY_CMDLINE_LIMIT Number of bytes to copy from the
command line.
",
);

Expand All @@ -58,6 +60,9 @@ const SHELL_VAR_FORMAT: &str = "TIMEHISTORY_FORMAT";
/// Shell variable to set the history limit.
const SHELL_VAR_LIMIT: &str = "TIMEHISTORY_LIMIT";

/// Shell variable to set the command line limit.
const SHELL_VAR_CMDLINE_LIMIT: &str = "TIMEHISTORY_CMDLINE_LIMIT";

struct TimeHistory;

#[derive(BuiltinOptions)]
Expand Down Expand Up @@ -101,6 +106,7 @@ impl TimeHistory {
}

variables::bind(SHELL_VAR_LIMIT, history::LimitVariable)?;
variables::bind(SHELL_VAR_CMDLINE_LIMIT, ipc::CmdLineLimitVariable)?;

procs::replace_functions()?;

Expand Down Expand Up @@ -156,7 +162,12 @@ impl Builtin for TimeHistory {
Opt::Reset => action = Action::Reset,

Opt::Setting(None) => {
self.print_config(&mut output, &history)?;
self.print_config(
&mut output,
&history,
ipc::global_shared_buffer(Duration::from_millis(100))
.map(|buf| buf.max_cmdline()),
)?;
exit_after_options = true;
}

Expand Down Expand Up @@ -280,17 +291,26 @@ impl Builtin for TimeHistory {
}

impl TimeHistory {
fn print_config(&self, mut output: impl Write, history: &history::History) -> io::Result<()> {
fn print_config(
&self,
mut output: impl Write,
history: &history::History,
max_cmdline: Option<usize>,
) -> io::Result<()> {
write!(
&mut output,
"\
TIMEHISTORY_FORMAT = {}\n\
TIMEHISTORY_LIMIT = {}\n\
TIMEHISTORY_FORMAT = {}\n\
TIMEHISTORY_LIMIT = {}\n\
",
Self::default_format(),
history.size(),
)?;

if let Some(max_cmdline) = max_cmdline {
writeln!(&mut output, "TIMEHISTORY_CMDLINE_LIMIT = {}", max_cmdline)?;
}

Ok(())
}

Expand Down
2 changes: 2 additions & 0 deletions src/procs/execve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ unsafe fn write_event(
) -> io::Result<()> {
let mut monotonic_time = MaybeUninit::zeroed();
let mut start_time = MaybeUninit::zeroed();
let max_cmdline = buffer.max_cmdline();

let pid = libc::getpid();
libc::clock_gettime(libc::CLOCK_MONOTONIC, monotonic_time.as_mut_ptr());
Expand All @@ -54,6 +55,7 @@ unsafe fn write_event(
start_time.assume_init(),
filename,
argv,
max_cmdline,
)?;

buffer.advance(written);
Expand Down
6 changes: 4 additions & 2 deletions src/tests/shell/change-config.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ load_builtin

TIMEHISTORY_LIMIT=5000
TIMEHISTORY_FORMAT='%n\t%P\t%C'
TIMEHISTORY_CMDLINE_LIMIT=1000

ASSERT_OUTPUT \
"timehistory -s" \
<<-'ITEMS'
TIMEHISTORY_FORMAT = %n\t%P\t%C
TIMEHISTORY_LIMIT = 5000
TIMEHISTORY_FORMAT = %n\t%P\t%C
TIMEHISTORY_LIMIT = 5000
TIMEHISTORY_CMDLINE_LIMIT = 1000
ITEMS

timehistory -s format='> %C'
Expand Down
15 changes: 15 additions & 0 deletions src/tests/shell/limit-cmdline.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Test to print a single entry from the history.

load_builtin

TIMEHISTORY_CMDLINE_LIMIT=50

/bin/true {1..10}
/bin/true {1000..1100}

ASSERT_OUTPUT \
"timehistory -f '%n %C'" \
<<-ITEMS
1 /bin/true 1 2 3 4 5 6 7 8 9 10
2 /bin/true 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1
ITEMS

0 comments on commit 82a1a74

Please sign in to comment.