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.
@@ -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