fanotify 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.yardopts +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +152 -0
- data/Rakefile +43 -0
- data/benchmark/events.rb +27 -0
- data/examples/audit_logger.rb +14 -0
- data/examples/lazy_fetch.rb +29 -0
- data/examples/malware_scan.rb +20 -0
- data/ext/fanotify/compat.h +235 -0
- data/ext/fanotify/constants.c +12 -0
- data/ext/fanotify/extconf.rb +10 -0
- data/ext/fanotify/fanotify.c +481 -0
- data/lib/fanotify/event.rb +215 -0
- data/lib/fanotify/file_handle.rb +12 -0
- data/lib/fanotify/mark.rb +6 -0
- data/lib/fanotify/notifier.rb +458 -0
- data/lib/fanotify/version.rb +5 -0
- data/lib/fanotify.rb +38 -0
- data/sig/fanotify.rbs +93 -0
- data/tools/constants.def +59 -0
- data/tools/dump_constants.c +12 -0
- data/tools/generate_constants.rb +18 -0
- data/tools/vm/Makefile +5 -0
- data/tools/vm/config-fragment +3 -0
- data/tools/vm/run.sh +14 -0
- metadata +70 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
#include "ruby.h"
|
|
2
|
+
#include "ruby/io.h"
|
|
3
|
+
#include "ruby/thread.h"
|
|
4
|
+
#include "compat.h"
|
|
5
|
+
|
|
6
|
+
#include <errno.h>
|
|
7
|
+
#include <fcntl.h>
|
|
8
|
+
#include <stdint.h>
|
|
9
|
+
#include <stdlib.h>
|
|
10
|
+
#include <string.h>
|
|
11
|
+
#ifndef _WIN32
|
|
12
|
+
#include <poll.h>
|
|
13
|
+
#include <unistd.h>
|
|
14
|
+
#endif
|
|
15
|
+
|
|
16
|
+
#define READ_BUFFER_SIZE (4096 * 16)
|
|
17
|
+
|
|
18
|
+
typedef struct { int fd; } descriptor_t;
|
|
19
|
+
struct owned_fd {
|
|
20
|
+
int fd;
|
|
21
|
+
VALUE descriptor;
|
|
22
|
+
struct owned_fd *next;
|
|
23
|
+
};
|
|
24
|
+
struct parse_context {
|
|
25
|
+
VALUE buffer;
|
|
26
|
+
VALUE descriptor_roots;
|
|
27
|
+
unsigned char *bytes;
|
|
28
|
+
size_t length;
|
|
29
|
+
int own_fds;
|
|
30
|
+
struct owned_fd *owned_head;
|
|
31
|
+
struct owned_fd *owned_tail;
|
|
32
|
+
struct owned_fd *owned_cursor;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
static VALUE mFanotify;
|
|
36
|
+
static VALUE mNative;
|
|
37
|
+
static VALUE cDescriptor;
|
|
38
|
+
static VALUE eError;
|
|
39
|
+
static VALUE eUnsupported;
|
|
40
|
+
|
|
41
|
+
void fanotify_define_constants(VALUE module);
|
|
42
|
+
|
|
43
|
+
static void descriptor_free(void *ptr)
|
|
44
|
+
{
|
|
45
|
+
descriptor_t *descriptor = ptr;
|
|
46
|
+
if (descriptor->fd >= 0) close(descriptor->fd);
|
|
47
|
+
xfree(descriptor);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static size_t descriptor_size(const void *ptr)
|
|
51
|
+
{
|
|
52
|
+
return ptr ? sizeof(descriptor_t) : 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static const rb_data_type_t descriptor_type = {
|
|
56
|
+
"Fanotify::Native::Descriptor",
|
|
57
|
+
{NULL, descriptor_free, descriptor_size, NULL},
|
|
58
|
+
NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
static VALUE descriptor_wrap(int fd)
|
|
62
|
+
{
|
|
63
|
+
descriptor_t *descriptor;
|
|
64
|
+
VALUE object = TypedData_Make_Struct(cDescriptor, descriptor_t, &descriptor_type, descriptor);
|
|
65
|
+
descriptor->fd = fd;
|
|
66
|
+
return object;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static VALUE tracked_descriptor(struct parse_context *context, int fd)
|
|
70
|
+
{
|
|
71
|
+
struct owned_fd *owned = malloc(sizeof(*owned));
|
|
72
|
+
if (!owned) {
|
|
73
|
+
close(fd);
|
|
74
|
+
rb_memerror();
|
|
75
|
+
}
|
|
76
|
+
owned->fd = fd;
|
|
77
|
+
owned->descriptor = Qnil;
|
|
78
|
+
owned->next = NULL;
|
|
79
|
+
if (context->owned_tail) context->owned_tail->next = owned;
|
|
80
|
+
else context->owned_head = owned;
|
|
81
|
+
context->owned_tail = owned;
|
|
82
|
+
owned->descriptor = descriptor_wrap(fd);
|
|
83
|
+
rb_ary_push(context->descriptor_roots, owned->descriptor);
|
|
84
|
+
return owned->descriptor;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static VALUE prepared_descriptor(struct parse_context *context, int fd)
|
|
88
|
+
{
|
|
89
|
+
struct owned_fd *owned = context->owned_cursor;
|
|
90
|
+
if (!owned || owned->fd != fd) rb_raise(rb_eRuntimeError, "fanotify descriptor tracking mismatch");
|
|
91
|
+
context->owned_cursor = owned->next;
|
|
92
|
+
return owned->descriptor;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
static void release_tracked_descriptors(struct parse_context *context, int close_fds)
|
|
96
|
+
{
|
|
97
|
+
struct owned_fd *owned = context->owned_head;
|
|
98
|
+
while (owned) {
|
|
99
|
+
struct owned_fd *next = owned->next;
|
|
100
|
+
if (close_fds) {
|
|
101
|
+
if (!NIL_P(owned->descriptor)) {
|
|
102
|
+
descriptor_t *descriptor;
|
|
103
|
+
TypedData_Get_Struct(owned->descriptor, descriptor_t, &descriptor_type, descriptor);
|
|
104
|
+
descriptor->fd = -1;
|
|
105
|
+
}
|
|
106
|
+
close(owned->fd);
|
|
107
|
+
}
|
|
108
|
+
free(owned);
|
|
109
|
+
owned = next;
|
|
110
|
+
}
|
|
111
|
+
context->owned_head = context->owned_tail = context->owned_cursor = NULL;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
static descriptor_t *get_descriptor(VALUE self)
|
|
115
|
+
{
|
|
116
|
+
descriptor_t *descriptor;
|
|
117
|
+
TypedData_Get_Struct(self, descriptor_t, &descriptor_type, descriptor);
|
|
118
|
+
if (descriptor->fd < 0) rb_raise(eError, "closed descriptor");
|
|
119
|
+
return descriptor;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static VALUE descriptor_close(VALUE self)
|
|
123
|
+
{
|
|
124
|
+
descriptor_t *descriptor;
|
|
125
|
+
TypedData_Get_Struct(self, descriptor_t, &descriptor_type, descriptor);
|
|
126
|
+
if (descriptor->fd >= 0) {
|
|
127
|
+
int fd = descriptor->fd;
|
|
128
|
+
descriptor->fd = -1;
|
|
129
|
+
if (close(fd) < 0) rb_sys_fail("close");
|
|
130
|
+
}
|
|
131
|
+
return Qnil;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static VALUE descriptor_closed_p(VALUE self)
|
|
135
|
+
{
|
|
136
|
+
descriptor_t *descriptor;
|
|
137
|
+
TypedData_Get_Struct(self, descriptor_t, &descriptor_type, descriptor);
|
|
138
|
+
return descriptor->fd < 0 ? Qtrue : Qfalse;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
static VALUE descriptor_fileno(VALUE self)
|
|
142
|
+
{
|
|
143
|
+
return INT2NUM(get_descriptor(self)->fd);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
static VALUE descriptor_to_io(VALUE self)
|
|
147
|
+
{
|
|
148
|
+
VALUE options = rb_hash_new();
|
|
149
|
+
VALUE args[2];
|
|
150
|
+
rb_hash_aset(options, ID2SYM(rb_intern("autoclose")), Qfalse);
|
|
151
|
+
args[0] = descriptor_fileno(self);
|
|
152
|
+
args[1] = options;
|
|
153
|
+
return rb_funcallv_kw(rb_cIO, rb_intern("for_fd"), 2, args, RB_PASS_KEYWORDS);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
struct read_args {
|
|
157
|
+
int fd;
|
|
158
|
+
int timeout_ms;
|
|
159
|
+
char *buffer;
|
|
160
|
+
ssize_t result;
|
|
161
|
+
int error;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
static void *read_without_gvl(void *pointer)
|
|
165
|
+
{
|
|
166
|
+
struct read_args *args = pointer;
|
|
167
|
+
#ifdef _WIN32
|
|
168
|
+
args->result = -1;
|
|
169
|
+
args->error = ENOSYS;
|
|
170
|
+
#else
|
|
171
|
+
struct pollfd pollfd = {args->fd, POLLIN, 0};
|
|
172
|
+
|
|
173
|
+
if (args->timeout_ms >= 0) {
|
|
174
|
+
int ready = poll(&pollfd, 1, args->timeout_ms);
|
|
175
|
+
if (ready <= 0) {
|
|
176
|
+
args->result = ready;
|
|
177
|
+
args->error = ready < 0 ? errno : 0;
|
|
178
|
+
return NULL;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
args->result = read(args->fd, args->buffer, READ_BUFFER_SIZE);
|
|
183
|
+
args->error = args->result < 0 ? errno : 0;
|
|
184
|
+
#endif
|
|
185
|
+
return NULL;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static VALUE perform_read(VALUE pointer)
|
|
189
|
+
{
|
|
190
|
+
struct read_args *args = (struct read_args *)pointer;
|
|
191
|
+
|
|
192
|
+
rb_thread_call_without_gvl(read_without_gvl, args, RUBY_UBF_IO, NULL);
|
|
193
|
+
if (args->result < 0 && (args->error == EAGAIN || args->error == EWOULDBLOCK)) {
|
|
194
|
+
return Qnil;
|
|
195
|
+
}
|
|
196
|
+
if (args->result < 0) {
|
|
197
|
+
rb_syserr_fail(args->error, "read(fanotify)");
|
|
198
|
+
}
|
|
199
|
+
if (args->result == 0) {
|
|
200
|
+
return Qnil;
|
|
201
|
+
}
|
|
202
|
+
return rb_str_new(args->buffer, args->result);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
static VALUE free_read_buffer(VALUE pointer)
|
|
206
|
+
{
|
|
207
|
+
struct read_args *args = (struct read_args *)pointer;
|
|
208
|
+
xfree(args->buffer);
|
|
209
|
+
return Qnil;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
static VALUE descriptor_read(VALUE self, VALUE timeout)
|
|
213
|
+
{
|
|
214
|
+
descriptor_t *descriptor = get_descriptor(self);
|
|
215
|
+
struct read_args args;
|
|
216
|
+
|
|
217
|
+
args.fd = descriptor->fd;
|
|
218
|
+
args.timeout_ms = NIL_P(timeout) ? -1 : NUM2INT(rb_funcall(timeout, rb_intern("*"), 1, INT2NUM(1000)));
|
|
219
|
+
if (args.timeout_ms < -1) rb_raise(rb_eArgError, "timeout must be non-negative");
|
|
220
|
+
args.buffer = ALLOC_N(char, READ_BUFFER_SIZE);
|
|
221
|
+
return rb_ensure(perform_read, (VALUE)&args, free_read_buffer, (VALUE)&args);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
static VALUE descriptor_mark(VALUE self, VALUE flags, VALUE mask, VALUE dirfd, VALUE path)
|
|
225
|
+
{
|
|
226
|
+
#ifdef __linux__
|
|
227
|
+
descriptor_t *descriptor = get_descriptor(self);
|
|
228
|
+
const char *pathname = NIL_P(path) ? NULL : StringValueCStr(path);
|
|
229
|
+
long result = syscall(SYS_fanotify_mark, descriptor->fd, NUM2UINT(flags), NUM2ULL(mask), NUM2INT(dirfd), pathname);
|
|
230
|
+
if (result < 0) rb_sys_fail("fanotify_mark");
|
|
231
|
+
return Qnil;
|
|
232
|
+
#else
|
|
233
|
+
(void)self; (void)flags; (void)mask; (void)dirfd; (void)path;
|
|
234
|
+
rb_raise(eUnsupported, "fanotify is only available on Linux");
|
|
235
|
+
#endif
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
static VALUE descriptor_respond(VALUE self, VALUE event_fd, VALUE response)
|
|
239
|
+
{
|
|
240
|
+
descriptor_t *descriptor = get_descriptor(self);
|
|
241
|
+
struct fanotify_response value = {NUM2INT(event_fd), NUM2UINT(response)};
|
|
242
|
+
ssize_t written;
|
|
243
|
+
do {
|
|
244
|
+
written = write(descriptor->fd, &value, sizeof(value));
|
|
245
|
+
} while (written < 0 && errno == EINTR);
|
|
246
|
+
if (written < 0) rb_sys_fail("write(fanotify response)");
|
|
247
|
+
if ((size_t)written != sizeof(value)) rb_raise(rb_eIOError, "short fanotify response write");
|
|
248
|
+
return Qnil;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
static VALUE native_supported_p(VALUE self)
|
|
252
|
+
{
|
|
253
|
+
(void)self;
|
|
254
|
+
#ifdef __linux__
|
|
255
|
+
int fd = (int)syscall(SYS_fanotify_init, FAN_CLASS_NOTIF | FAN_CLOEXEC | FAN_NONBLOCK,
|
|
256
|
+
O_RDONLY | O_CLOEXEC | O_LARGEFILE);
|
|
257
|
+
if (fd >= 0) {
|
|
258
|
+
close(fd);
|
|
259
|
+
return Qtrue;
|
|
260
|
+
}
|
|
261
|
+
return (errno == ENOSYS || errno == EOPNOTSUPP) ? Qfalse : Qtrue;
|
|
262
|
+
#else
|
|
263
|
+
return Qfalse;
|
|
264
|
+
#endif
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
static VALUE native_open(VALUE self, VALUE flags, VALUE event_flags)
|
|
268
|
+
{
|
|
269
|
+
(void)self;
|
|
270
|
+
#ifdef __linux__
|
|
271
|
+
int fd = (int)syscall(SYS_fanotify_init, NUM2UINT(flags), NUM2UINT(event_flags));
|
|
272
|
+
if (fd < 0) {
|
|
273
|
+
if (errno == ENOSYS || errno == EOPNOTSUPP) rb_raise(eUnsupported, "fanotify is not supported by this kernel");
|
|
274
|
+
rb_sys_fail("fanotify_init");
|
|
275
|
+
}
|
|
276
|
+
return descriptor_wrap(fd);
|
|
277
|
+
#else
|
|
278
|
+
(void)flags; (void)event_flags;
|
|
279
|
+
rb_raise(eUnsupported, "fanotify is only available on Linux");
|
|
280
|
+
#endif
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
static VALUE hash(void)
|
|
284
|
+
{
|
|
285
|
+
return rb_hash_new();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
static void set(VALUE target, const char *key, VALUE value)
|
|
289
|
+
{
|
|
290
|
+
rb_hash_aset(target, ID2SYM(rb_intern(key)), value);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
static VALUE parse_info(const unsigned char *bytes, size_t length, struct parse_context *context)
|
|
294
|
+
{
|
|
295
|
+
struct fanotify_event_info_header header;
|
|
296
|
+
VALUE info = hash();
|
|
297
|
+
memcpy(&header, bytes, sizeof(header));
|
|
298
|
+
set(info, "type", UINT2NUM(header.info_type));
|
|
299
|
+
|
|
300
|
+
if (header.info_type == FAN_EVENT_INFO_TYPE_PIDFD) {
|
|
301
|
+
int32_t pidfd;
|
|
302
|
+
if (length < sizeof(header) + sizeof(pidfd)) rb_raise(rb_eArgError, "truncated PIDFD info record");
|
|
303
|
+
memcpy(&pidfd, bytes + sizeof(header), sizeof(pidfd));
|
|
304
|
+
set(info, "pidfd", INT2NUM(pidfd));
|
|
305
|
+
if (context->own_fds && pidfd >= 0)
|
|
306
|
+
set(info, "pidfd_descriptor", prepared_descriptor(context, pidfd));
|
|
307
|
+
} else if (header.info_type == FAN_EVENT_INFO_TYPE_ERROR) {
|
|
308
|
+
int32_t error;
|
|
309
|
+
uint32_t count;
|
|
310
|
+
if (length < sizeof(header) + sizeof(error) + sizeof(count)) rb_raise(rb_eArgError, "truncated ERROR info record");
|
|
311
|
+
memcpy(&error, bytes + sizeof(header), sizeof(error));
|
|
312
|
+
memcpy(&count, bytes + sizeof(header) + sizeof(error), sizeof(count));
|
|
313
|
+
set(info, "error", INT2NUM(error));
|
|
314
|
+
set(info, "error_count", UINT2NUM(count));
|
|
315
|
+
} else if (header.info_type == FAN_EVENT_INFO_TYPE_FID ||
|
|
316
|
+
header.info_type == FAN_EVENT_INFO_TYPE_DFID ||
|
|
317
|
+
header.info_type == FAN_EVENT_INFO_TYPE_DFID_NAME ||
|
|
318
|
+
header.info_type == FAN_EVENT_INFO_TYPE_OLD_DFID_NAME ||
|
|
319
|
+
header.info_type == FAN_EVENT_INFO_TYPE_NEW_DFID_NAME) {
|
|
320
|
+
uint32_t handle_bytes;
|
|
321
|
+
int32_t handle_type;
|
|
322
|
+
size_t fixed = sizeof(header) + 8 + sizeof(handle_bytes) + sizeof(handle_type);
|
|
323
|
+
size_t name_offset;
|
|
324
|
+
const unsigned char *nul;
|
|
325
|
+
if (length < fixed) rb_raise(rb_eArgError, "truncated FID info record");
|
|
326
|
+
memcpy(&handle_bytes, bytes + sizeof(header) + 8, sizeof(handle_bytes));
|
|
327
|
+
memcpy(&handle_type, bytes + sizeof(header) + 8 + sizeof(handle_bytes), sizeof(handle_type));
|
|
328
|
+
if (handle_bytes > length - fixed) rb_raise(rb_eArgError, "invalid file handle length");
|
|
329
|
+
set(info, "fsid", rb_str_new((const char *)bytes + sizeof(header), 8));
|
|
330
|
+
set(info, "handle_type", INT2NUM(handle_type));
|
|
331
|
+
set(info, "handle", rb_str_new((const char *)bytes + fixed, handle_bytes));
|
|
332
|
+
name_offset = fixed + handle_bytes;
|
|
333
|
+
if (header.info_type == FAN_EVENT_INFO_TYPE_DFID_NAME ||
|
|
334
|
+
header.info_type == FAN_EVENT_INFO_TYPE_OLD_DFID_NAME ||
|
|
335
|
+
header.info_type == FAN_EVENT_INFO_TYPE_NEW_DFID_NAME) {
|
|
336
|
+
nul = memchr(bytes + name_offset, '\0', length - name_offset);
|
|
337
|
+
if (!nul) rb_raise(rb_eArgError, "unterminated DFID name");
|
|
338
|
+
set(info, "name", rb_str_new((const char *)bytes + name_offset, nul - (bytes + name_offset)));
|
|
339
|
+
}
|
|
340
|
+
} else if (header.info_type == FAN_EVENT_INFO_TYPE_RANGE) {
|
|
341
|
+
uint64_t offset;
|
|
342
|
+
uint64_t count;
|
|
343
|
+
if (length < sizeof(header) + 4 + sizeof(offset) + sizeof(count))
|
|
344
|
+
rb_raise(rb_eArgError, "truncated RANGE info record");
|
|
345
|
+
memcpy(&offset, bytes + sizeof(header) + 4, sizeof(offset));
|
|
346
|
+
memcpy(&count, bytes + sizeof(header) + 4 + sizeof(offset), sizeof(count));
|
|
347
|
+
set(info, "range_offset", ULL2NUM(offset));
|
|
348
|
+
set(info, "range_count", ULL2NUM(count));
|
|
349
|
+
} else {
|
|
350
|
+
set(info, "raw", rb_str_new((const char *)bytes + sizeof(header), length - sizeof(header)));
|
|
351
|
+
}
|
|
352
|
+
return info;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
static void prepare_descriptors(struct parse_context *context, const unsigned char *bytes, size_t length)
|
|
356
|
+
{
|
|
357
|
+
size_t offset = 0;
|
|
358
|
+
|
|
359
|
+
while (offset < length) {
|
|
360
|
+
struct fanotify_event_metadata metadata;
|
|
361
|
+
size_t info_offset;
|
|
362
|
+
if (length - offset < sizeof(metadata)) break;
|
|
363
|
+
memcpy(&metadata, bytes + offset, sizeof(metadata));
|
|
364
|
+
if (metadata.fd >= 0) tracked_descriptor(context, metadata.fd);
|
|
365
|
+
if (metadata.metadata_len < sizeof(metadata) || metadata.event_len < metadata.metadata_len ||
|
|
366
|
+
metadata.event_len > length - offset) break;
|
|
367
|
+
|
|
368
|
+
info_offset = offset + metadata.metadata_len;
|
|
369
|
+
while (info_offset < offset + metadata.event_len) {
|
|
370
|
+
struct fanotify_event_info_header header;
|
|
371
|
+
int32_t pidfd;
|
|
372
|
+
if (offset + metadata.event_len - info_offset < sizeof(header)) break;
|
|
373
|
+
memcpy(&header, bytes + info_offset, sizeof(header));
|
|
374
|
+
if (header.len < sizeof(header) || header.len > offset + metadata.event_len - info_offset) break;
|
|
375
|
+
if (header.info_type == FAN_EVENT_INFO_TYPE_PIDFD && header.len >= sizeof(header) + sizeof(pidfd)) {
|
|
376
|
+
memcpy(&pidfd, bytes + info_offset + sizeof(header), sizeof(pidfd));
|
|
377
|
+
if (pidfd >= 0) tracked_descriptor(context, pidfd);
|
|
378
|
+
}
|
|
379
|
+
info_offset += header.len;
|
|
380
|
+
}
|
|
381
|
+
offset += metadata.event_len;
|
|
382
|
+
}
|
|
383
|
+
context->owned_cursor = context->owned_head;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
static VALUE parse_buffer(VALUE context_value)
|
|
387
|
+
{
|
|
388
|
+
struct parse_context *context = (struct parse_context *)context_value;
|
|
389
|
+
const unsigned char *bytes = context->bytes;
|
|
390
|
+
size_t length = context->length;
|
|
391
|
+
size_t offset = 0;
|
|
392
|
+
VALUE events = rb_ary_new();
|
|
393
|
+
|
|
394
|
+
if (context->own_fds) prepare_descriptors(context, bytes, length);
|
|
395
|
+
|
|
396
|
+
while (offset < length) {
|
|
397
|
+
struct fanotify_event_metadata metadata;
|
|
398
|
+
size_t info_offset;
|
|
399
|
+
VALUE event;
|
|
400
|
+
VALUE infos;
|
|
401
|
+
|
|
402
|
+
if (length - offset < sizeof(metadata)) rb_raise(rb_eArgError, "truncated fanotify metadata");
|
|
403
|
+
memcpy(&metadata, bytes + offset, sizeof(metadata));
|
|
404
|
+
if (metadata.vers != FANOTIFY_METADATA_VERSION) rb_raise(rb_eArgError, "unsupported fanotify metadata version");
|
|
405
|
+
if (metadata.metadata_len < sizeof(metadata) || metadata.event_len < metadata.metadata_len)
|
|
406
|
+
rb_raise(rb_eArgError, "invalid fanotify event length");
|
|
407
|
+
if (metadata.event_len > length - offset) rb_raise(rb_eArgError, "truncated fanotify event");
|
|
408
|
+
|
|
409
|
+
event = hash();
|
|
410
|
+
infos = rb_ary_new();
|
|
411
|
+
set(event, "raw_mask", ULL2NUM(metadata.mask));
|
|
412
|
+
set(event, "fd", INT2NUM(metadata.fd));
|
|
413
|
+
set(event, "pid", INT2NUM(metadata.pid));
|
|
414
|
+
set(event, "infos", infos);
|
|
415
|
+
if (context->own_fds && metadata.fd >= 0)
|
|
416
|
+
set(event, "file_descriptor", prepared_descriptor(context, metadata.fd));
|
|
417
|
+
|
|
418
|
+
info_offset = offset + metadata.metadata_len;
|
|
419
|
+
while (info_offset < offset + metadata.event_len) {
|
|
420
|
+
struct fanotify_event_info_header header;
|
|
421
|
+
if (offset + metadata.event_len - info_offset < sizeof(header))
|
|
422
|
+
rb_raise(rb_eArgError, "truncated fanotify info header");
|
|
423
|
+
memcpy(&header, bytes + info_offset, sizeof(header));
|
|
424
|
+
if (header.len < sizeof(header) || header.len > offset + metadata.event_len - info_offset)
|
|
425
|
+
rb_raise(rb_eArgError, "invalid fanotify info length");
|
|
426
|
+
rb_ary_push(infos, parse_info(bytes + info_offset, header.len, context));
|
|
427
|
+
info_offset += header.len;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
rb_ary_push(events, event);
|
|
431
|
+
offset += metadata.event_len;
|
|
432
|
+
}
|
|
433
|
+
return events;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
static VALUE native_parse(int argc, VALUE *argv, VALUE self)
|
|
437
|
+
{
|
|
438
|
+
VALUE own_fds_value;
|
|
439
|
+
VALUE events;
|
|
440
|
+
int exception;
|
|
441
|
+
struct parse_context context = {Qnil, Qnil, NULL, 0, 0, NULL, NULL, NULL};
|
|
442
|
+
(void)self;
|
|
443
|
+
|
|
444
|
+
rb_scan_args(argc, argv, "11", &context.buffer, &own_fds_value);
|
|
445
|
+
StringValue(context.buffer);
|
|
446
|
+
context.length = (size_t)RSTRING_LEN(context.buffer);
|
|
447
|
+
context.descriptor_roots = rb_ary_new();
|
|
448
|
+
context.bytes = ALLOC_N(unsigned char, context.length ? context.length : 1);
|
|
449
|
+
memcpy(context.bytes, RSTRING_PTR(context.buffer), context.length);
|
|
450
|
+
context.own_fds = RTEST(own_fds_value);
|
|
451
|
+
rb_gc_register_address(&context.descriptor_roots);
|
|
452
|
+
events = rb_protect(parse_buffer, (VALUE)&context, &exception);
|
|
453
|
+
release_tracked_descriptors(&context, exception);
|
|
454
|
+
rb_gc_unregister_address(&context.descriptor_roots);
|
|
455
|
+
xfree(context.bytes);
|
|
456
|
+
if (exception) rb_jump_tag(exception);
|
|
457
|
+
RB_GC_GUARD(context.buffer);
|
|
458
|
+
RB_GC_GUARD(context.descriptor_roots);
|
|
459
|
+
return events;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
void Init_fanotify(void)
|
|
463
|
+
{
|
|
464
|
+
mFanotify = rb_define_module("Fanotify");
|
|
465
|
+
eError = rb_const_get(mFanotify, rb_intern("Error"));
|
|
466
|
+
eUnsupported = rb_const_get(mFanotify, rb_intern("UnsupportedError"));
|
|
467
|
+
mNative = rb_define_module_under(mFanotify, "Native");
|
|
468
|
+
cDescriptor = rb_define_class_under(mNative, "Descriptor", rb_cObject);
|
|
469
|
+
rb_undef_alloc_func(cDescriptor);
|
|
470
|
+
rb_define_method(cDescriptor, "close", descriptor_close, 0);
|
|
471
|
+
rb_define_method(cDescriptor, "closed?", descriptor_closed_p, 0);
|
|
472
|
+
rb_define_method(cDescriptor, "fileno", descriptor_fileno, 0);
|
|
473
|
+
rb_define_method(cDescriptor, "to_io", descriptor_to_io, 0);
|
|
474
|
+
rb_define_method(cDescriptor, "read", descriptor_read, 1);
|
|
475
|
+
rb_define_method(cDescriptor, "mark", descriptor_mark, 4);
|
|
476
|
+
rb_define_method(cDescriptor, "respond", descriptor_respond, 2);
|
|
477
|
+
rb_define_singleton_method(mNative, "supported?", native_supported_p, 0);
|
|
478
|
+
rb_define_singleton_method(mNative, "open", native_open, 2);
|
|
479
|
+
rb_define_singleton_method(mNative, "parse", native_parse, -1);
|
|
480
|
+
fanotify_define_constants(mNative);
|
|
481
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fanotify
|
|
4
|
+
# One parsed fanotify event and any descriptors owned by it.
|
|
5
|
+
class Event
|
|
6
|
+
INFO_FID = 1
|
|
7
|
+
INFO_DFID_NAME = 2
|
|
8
|
+
INFO_DFID = 3
|
|
9
|
+
INFO_PIDFD = 4
|
|
10
|
+
INFO_ERROR = 5
|
|
11
|
+
INFO_RANGE = 6
|
|
12
|
+
INFO_OLD_DFID_NAME = 10
|
|
13
|
+
INFO_NEW_DFID_NAME = 12
|
|
14
|
+
|
|
15
|
+
attr_reader :raw_mask, :pid, :notifier, :fid, :dfid, :name, :pidfd,
|
|
16
|
+
:pidfd_error, :old_dfid, :old_name, :new_dfid, :new_name,
|
|
17
|
+
:error, :error_count, :range_offset, :range_count
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def parse(buffer)
|
|
21
|
+
Native.parse(buffer).map { |attributes| new(attributes) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def parse_owned(buffer, notifier:)
|
|
27
|
+
attributes = Native.parse(buffer, true)
|
|
28
|
+
events = []
|
|
29
|
+
attributes.each { |item| events << new(item, notifier:, own_fds: true) }
|
|
30
|
+
events
|
|
31
|
+
rescue Exception # Transferred kernel descriptors must not wait for GC on construction failure.
|
|
32
|
+
events&.each { |event| close_owned(event) }
|
|
33
|
+
attributes&.each do |item|
|
|
34
|
+
close_owned(item[:file_descriptor])
|
|
35
|
+
item.fetch(:infos, []).each { |info| close_owned(info[:pidfd_descriptor]) }
|
|
36
|
+
end
|
|
37
|
+
raise
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def close_owned(resource)
|
|
41
|
+
resource&.close unless resource&.closed?
|
|
42
|
+
rescue Exception
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def initialize(attributes, notifier: nil, own_fds: false)
|
|
48
|
+
@raw_mask = attributes.fetch(:raw_mask)
|
|
49
|
+
@pid = attributes[:pid].negative? ? nil : attributes[:pid]
|
|
50
|
+
@notifier = notifier
|
|
51
|
+
@event_fd = attributes[:fd] unless attributes[:fd] == FAN_NOFD
|
|
52
|
+
@response_mutex = Mutex.new
|
|
53
|
+
@file_descriptor = attributes[:file_descriptor] if own_fds
|
|
54
|
+
attributes.fetch(:infos).each { |info| apply_info(info, own_fds:) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def mask
|
|
58
|
+
EVENT_MASKS.filter_map { |name, value| name if (raw_mask & value).positive? }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def file
|
|
62
|
+
@response_mutex.synchronize do
|
|
63
|
+
raise Error, "event is closed" if closed?
|
|
64
|
+
|
|
65
|
+
@file ||= @file_descriptor&.to_io&.dup
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def path
|
|
70
|
+
@response_mutex.synchronize do
|
|
71
|
+
raise Error, "event is closed" if closed?
|
|
72
|
+
return unless @event_fd
|
|
73
|
+
|
|
74
|
+
File.readlink("/proc/self/fd/#{@event_fd}")
|
|
75
|
+
end
|
|
76
|
+
rescue SystemCallError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def process_path
|
|
81
|
+
return unless pid
|
|
82
|
+
|
|
83
|
+
File.readlink("/proc/#{pid}/exe")
|
|
84
|
+
rescue SystemCallError
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def deleted? = path&.end_with?(" (deleted)") || false
|
|
89
|
+
def overflow? = (raw_mask & FAN_Q_OVERFLOW).positive?
|
|
90
|
+
def permission? = (raw_mask & Notifier::PERMISSION_MASK).positive?
|
|
91
|
+
def responded? = @responded || false
|
|
92
|
+
def deferred? = @deferred || false
|
|
93
|
+
def closed? = @closed || false
|
|
94
|
+
|
|
95
|
+
def allow!(audit: false) = respond!(FAN_ALLOW, audit:)
|
|
96
|
+
def deny!(audit: false) = respond!(FAN_DENY, audit:)
|
|
97
|
+
|
|
98
|
+
def defer!
|
|
99
|
+
raise Error, "only permission events can be deferred" unless permission?
|
|
100
|
+
raise Error, "event is not associated with a notifier" unless notifier
|
|
101
|
+
|
|
102
|
+
@response_mutex.synchronize do
|
|
103
|
+
raise Error, "event is closed" if closed?
|
|
104
|
+
raise Error, "event has already been responded to" if responded?
|
|
105
|
+
return self if deferred?
|
|
106
|
+
|
|
107
|
+
Thread.handle_interrupt(Exception => :never) do
|
|
108
|
+
@pending_token = notifier.__send__(:register_pending, self, @event_fd,
|
|
109
|
+
[@file_descriptor, @pidfd_descriptor].compact)
|
|
110
|
+
@deferred = true
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
self
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def close
|
|
117
|
+
@response_mutex.synchronize do
|
|
118
|
+
return if closed?
|
|
119
|
+
|
|
120
|
+
begin
|
|
121
|
+
respond_without_lock(FAN_ALLOW, audit: false) if permission? && !responded? && notifier
|
|
122
|
+
ensure
|
|
123
|
+
active_error = $!
|
|
124
|
+
close_error = nil
|
|
125
|
+
if deferred?
|
|
126
|
+
begin
|
|
127
|
+
notifier.__send__(:unregister_pending, @pending_token)
|
|
128
|
+
rescue StandardError => error
|
|
129
|
+
close_error ||= error
|
|
130
|
+
ensure
|
|
131
|
+
@pending_token = nil
|
|
132
|
+
@deferred = false
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
[@file, @file_descriptor, @pidfd_descriptor].each do |resource|
|
|
136
|
+
resource&.close unless resource&.closed?
|
|
137
|
+
rescue StandardError => error
|
|
138
|
+
close_error ||= error
|
|
139
|
+
end
|
|
140
|
+
@closed = true
|
|
141
|
+
raise close_error if !active_error && close_error
|
|
142
|
+
end
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
rescue Exception # Closing the group is the only remaining fail-open response.
|
|
146
|
+
fail_open_notifier
|
|
147
|
+
raise
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def respond!(response, audit:)
|
|
153
|
+
raise Error, "not a permission event" unless permission?
|
|
154
|
+
raise Error, "event is not associated with a notifier" unless notifier
|
|
155
|
+
raise ArgumentError, "audit must be true or false" unless audit == true || audit == false
|
|
156
|
+
|
|
157
|
+
begin
|
|
158
|
+
@response_mutex.synchronize do
|
|
159
|
+
raise Error, "event is closed" if closed?
|
|
160
|
+
raise Error, "event has already been responded to" if responded?
|
|
161
|
+
|
|
162
|
+
respond_without_lock(response, audit:)
|
|
163
|
+
end
|
|
164
|
+
rescue Exception
|
|
165
|
+
fail_open_notifier
|
|
166
|
+
begin
|
|
167
|
+
close unless responded? || closed?
|
|
168
|
+
rescue Exception
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
raise
|
|
172
|
+
end
|
|
173
|
+
self
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def respond_without_lock(response, audit:)
|
|
177
|
+
Thread.handle_interrupt(Exception => :never) do
|
|
178
|
+
notifier.__send__(:respond_to_event, @event_fd, response | (audit ? FAN_AUDIT : 0))
|
|
179
|
+
@responded = true
|
|
180
|
+
notifier.__send__(:unregister_pending, @pending_token)
|
|
181
|
+
@pending_token = nil
|
|
182
|
+
@deferred = false
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def fail_open_notifier
|
|
187
|
+
notifier.close if permission? && !responded? && notifier
|
|
188
|
+
rescue Exception
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def apply_info(info, own_fds:)
|
|
193
|
+
case info.fetch(:type)
|
|
194
|
+
when INFO_FID then @fid = file_handle(info)
|
|
195
|
+
when INFO_DFID then @dfid = file_handle(info)
|
|
196
|
+
when INFO_DFID_NAME then @dfid, @name = file_handle(info), info[:name]
|
|
197
|
+
when INFO_OLD_DFID_NAME then @old_dfid, @old_name = file_handle(info), info[:name]
|
|
198
|
+
when INFO_NEW_DFID_NAME then @new_dfid, @new_name = file_handle(info), info[:name]
|
|
199
|
+
when INFO_PIDFD
|
|
200
|
+
if info[:pidfd].negative?
|
|
201
|
+
@pidfd_error = info[:pidfd]
|
|
202
|
+
else
|
|
203
|
+
@pidfd = info[:pidfd]
|
|
204
|
+
@pidfd_descriptor = info[:pidfd_descriptor] if own_fds
|
|
205
|
+
end
|
|
206
|
+
when INFO_ERROR then @error, @error_count = info.values_at(:error, :error_count)
|
|
207
|
+
when INFO_RANGE then @range_offset, @range_count = info.values_at(:range_offset, :range_count)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def file_handle(info)
|
|
212
|
+
FileHandle.new(fsid: info.fetch(:fsid), type: info.fetch(:handle_type), bytes: info.fetch(:handle))
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fanotify
|
|
4
|
+
# Opaque filesystem identifier reported by the kernel.
|
|
5
|
+
FileHandle = Data.define(:fsid, :type, :bytes) do
|
|
6
|
+
def initialize(fsid:, type:, bytes:)
|
|
7
|
+
super(fsid: fsid.b.freeze, type:, bytes: bytes.b.freeze)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def to_s = bytes.unpack1("H*")
|
|
11
|
+
end
|
|
12
|
+
end
|