Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,38 @@ All notable changes to this project will be documented in this file.

## Unreleased

### Added

- `Landlock.capture` results now report monotonic elapsed time and user CPU, system CPU, total CPU, and peak resident memory usage for the direct child selected by `wait4`. Operating-system accounting may include descendants that child reaped. Resource usage remains available for unsuccessful exits, signals, timeouts, and output-limit termination.

```ruby
result = Landlock.capture(
["magick", input_path, "-resize", "800x800>", output_path],
read: [input_path, "/usr", "/lib", "/lib64"],
write: [File.dirname(output_path)],
execute: ["/usr", "/lib", "/lib64"]
)

usage = result.resource_usage

result.elapsed_seconds # Wall-clock seconds
usage.user_seconds # CPU seconds spent in user mode
usage.system_seconds # CPU seconds spent in kernel mode
usage.cpu_seconds # user_seconds + system_seconds
usage.max_rss_bytes # Peak resident memory in bytes
```

`Landlock.capture!` exposes the same measurements on failed commands through `Landlock::CommandError#result`:

```ruby
begin
Landlock.capture!(command, rlimits: { cpu_seconds: 5 })
rescue Landlock::CommandError => error
error.result.elapsed_seconds
error.result.resource_usage.cpu_seconds
end
```

## [0.4] - 2026-08-10

### Changed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ result = Landlock.capture(
metadata = JSON.parse(result.stdout) if result.success?
```

`Landlock.capture` takes the command as a single argv array, like `Landlock.exec`. It returns a `Landlock::CaptureResult` with `stdout`, `stderr`, `status`, `success?`, `timed_out?`, and `output_truncated?`, including for unsuccessful exit statuses. It also supports array destructuring:
`Landlock.capture` takes the command as a single argv array, like `Landlock.exec`. It returns a `Landlock::CaptureResult` with `stdout`, `stderr`, `status`, `success?`, `timed_out?`, `output_truncated?`, `elapsed_seconds`, and `resource_usage` for the direct child selected by `wait4`, including for unsuccessful exit statuses. Operating-system accounting may include descendants that child reaped. Resource usage exposes `user_seconds`, `system_seconds`, their sum as `cpu_seconds`, and `max_rss_bytes`. It also supports array destructuring:

```ruby
stdout, stderr, status = Landlock.capture(
Expand Down
2 changes: 2 additions & 0 deletions ext/landlock/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
have_header("sys/prctl.h")
have_header("sys/syscall.h")
have_header("sys/resource.h")
have_header("sys/wait.h")
have_header("fcntl.h")
have_func("wait4", %w[sys/resource.h sys/wait.h])

create_makefile("landlock/landlock")

Expand Down
57 changes: 57 additions & 0 deletions ext/landlock/landlock.c
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
#include "ruby.h"
#include "ruby/thread.h"
#include "landlock_native.h"
#include "seccomp_deny_network.h"

#include <string.h>
#include <sys/resource.h>
#include <sys/wait.h>

static VALUE mLandlock;
static VALUE eLandlockError;
static VALUE eSyscallError;

struct rb_landlock_wait4_args {
pid_t pid;
int flags;
int status;
int error_number;
struct rusage usage;
pid_t waited_pid;
};

static void raise_syscall_error(const char *syscall_name) {
int saved_errno = errno;
VALUE err = rb_funcall(eSyscallError, rb_intern("new"), 3, rb_str_new_cstr(syscall_name),
Expand Down Expand Up @@ -129,6 +141,50 @@ static VALUE rb_ll_seccomp_deny_network(VALUE self) {
return Qtrue;
}

#ifdef HAVE_WAIT4
static void *ll_wait4_without_gvl(void *pointer) {
struct rb_landlock_wait4_args *args = pointer;
args->waited_pid = wait4(args->pid, &args->status, args->flags, &args->usage);
args->error_number = args->waited_pid < 0 ? errno : 0;
return NULL;
}
#endif

static VALUE rb_ll_wait4(VALUE self, VALUE pid_value, VALUE flags_value) {
#ifdef HAVE_WAIT4
struct rb_landlock_wait4_args args;
args.pid = (pid_t)NUM2LONG(pid_value);
args.flags = NUM2INT(flags_value);

do {
rb_thread_call_without_gvl(ll_wait4_without_gvl, &args, RUBY_UBF_IO, NULL);
} while (args.waited_pid < 0 && args.error_number == EINTR);

if (args.waited_pid == 0) {
return Qnil;
}
if (args.waited_pid < 0) {
rb_syserr_fail(args.error_number, "wait4");
}

rb_last_status_set(args.status, args.waited_pid);

VALUE user_seconds =
DBL2NUM((double)args.usage.ru_utime.tv_sec + (double)args.usage.ru_utime.tv_usec / 1000000.0);
VALUE system_seconds =
DBL2NUM((double)args.usage.ru_stime.tv_sec + (double)args.usage.ru_stime.tv_usec / 1000000.0);
#ifdef __linux__
VALUE max_rss_bytes = ULL2NUM((unsigned long long)args.usage.ru_maxrss * 1024ULL);
#else
VALUE max_rss_bytes = ULL2NUM((unsigned long long)args.usage.ru_maxrss);
#endif

return rb_ary_new_from_args(4, rb_last_status_get(), user_seconds, system_seconds, max_rss_bytes);
#else
rb_raise(rb_eNotImpError, "wait4 is unavailable on this platform");
#endif
}

void Init_landlock(void) {
mLandlock = rb_define_module("Landlock");

Expand All @@ -150,6 +206,7 @@ void Init_landlock(void) {
rb_define_singleton_method(mLandlock, "_add_net_rule", rb_ll_add_net_rule, 3);
rb_define_singleton_method(mLandlock, "_restrict_self", rb_ll_restrict_self, 1);
rb_define_singleton_method(mLandlock, "_close_fd", rb_ll_close_fd, 1);
rb_define_singleton_method(mLandlock, "_wait4", rb_ll_wait4, 2);
rb_define_singleton_method(mLandlock, "seccomp_deny_network!", rb_ll_seccomp_deny_network, 0);

rb_define_const(mLandlock, "ACCESS_FS_EXECUTE", ULL2NUM(LANDLOCK_ACCESS_FS_EXECUTE));
Expand Down
9 changes: 9 additions & 0 deletions lib/landlock/native.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require_relative "errors"
require_relative "landlock"
require_relative "result"

module Landlock
module Native
Expand Down Expand Up @@ -31,6 +32,14 @@ def close_fd(fd)
Landlock.__send__(:_close_fd, fd)
end

def wait4(pid, flags)
result = Landlock.__send__(:_wait4, pid, flags)
return unless result

status, user_seconds, system_seconds, max_rss_bytes = result
[status, ResourceUsage.new(user_seconds:, system_seconds:, max_rss_bytes:)]
end

def seccomp_deny_network!
Landlock.seccomp_deny_network!
end
Expand Down
Loading
Loading