├── LICENSE ├── README.md ├── example └── main.go └── fanotify ├── fanotify.go ├── go.mod └── go.sum /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2024 s3rj1k 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-fanotify 2 | Golang fanotify example 3 | 4 | ### Useful links 5 | * https://launchpad.net/fatrace 6 | * https://github.com/amir73il/ltp/blob/master/testcases/kernel/syscalls/fanotify/fanotify15.c 7 | 8 | ### Authors 9 | Code was based on: `github.com/docker-slim/docker-slim/tree/master/pkg/third_party/madmo/fanotify` 10 | 11 | Original author: `Moritz Bitsch , 2012` 12 | 13 | Code refactor by: `s3rj1k , 2019` 14 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/s3rj1k/go-fanotify/fanotify" 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | func main() { 13 | log.SetFlags(log.Lshortfile) 14 | 15 | notify, err := fanotify.Initialize( 16 | unix.FAN_CLOEXEC| 17 | unix.FAN_CLASS_NOTIF| 18 | unix.FAN_UNLIMITED_QUEUE| 19 | unix.FAN_UNLIMITED_MARKS, 20 | os.O_RDONLY| 21 | unix.O_LARGEFILE| 22 | unix.O_CLOEXEC, 23 | ) 24 | if err != nil { 25 | log.Fatalf("%v\n", err) 26 | } 27 | 28 | var mountpoint string 29 | 30 | if val, ok := os.LookupEnv("MOUNT_POINT"); !ok { 31 | mountpoint = "/" 32 | } else { 33 | mountpoint = val 34 | } 35 | 36 | if err = notify.Mark( 37 | unix.FAN_MARK_ADD| 38 | unix.FAN_MARK_MOUNT, 39 | unix.FAN_MODIFY| 40 | unix.FAN_CLOSE_WRITE, 41 | unix.AT_FDCWD, 42 | mountpoint, 43 | ); err != nil { 44 | log.Fatalf("%v\n", err) 45 | } 46 | 47 | f := func(notify *fanotify.NotifyFD) (string, error) { 48 | data, err := notify.GetEvent(os.Getpid()) 49 | if err != nil { 50 | return "", fmt.Errorf("%w", err) 51 | } 52 | 53 | if data == nil { 54 | return "", nil 55 | } 56 | 57 | defer data.Close() 58 | 59 | path, err := data.GetPath() 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | dataFile := data.File() 65 | defer dataFile.Close() 66 | 67 | fInfo, err := dataFile.Stat() 68 | if err != nil { 69 | return "", err 70 | } 71 | 72 | mTime := fInfo.ModTime() 73 | 74 | if data.MatchMask(unix.FAN_CLOSE_WRITE) || data.MatchMask(unix.FAN_MODIFY) { 75 | return fmt.Sprintf("PID:%d %s - %v", data.GetPID(), path, mTime), nil 76 | } 77 | 78 | return "", fmt.Errorf("fanotify: unknown event") 79 | } 80 | 81 | for { 82 | str, err := f(notify) 83 | if err == nil && len(str) > 0 { 84 | fmt.Printf("%s\n", str) 85 | } 86 | 87 | if err != nil { 88 | fmt.Printf("error: %v\n", err) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /fanotify/fanotify.go: -------------------------------------------------------------------------------- 1 | // Package fanotify package provides a simple fanotify API. 2 | package fanotify 3 | 4 | import ( 5 | "bufio" 6 | "bytes" 7 | "encoding/binary" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "os" 12 | "path/filepath" 13 | "strconv" 14 | "strings" 15 | 16 | "golang.org/x/sys/unix" 17 | ) 18 | 19 | // Procfs constants. 20 | const ( 21 | ProcFsFd = "/proc/self/fd" 22 | ProcFsFdInfo = "/proc/self/fdinfo" 23 | ) 24 | 25 | // FdInfo describes '/proc/PID/fdinfo/%d'. 26 | type FdInfo struct { 27 | Position int 28 | Flags int // octal 29 | MountID int 30 | } 31 | 32 | // EventMetadata is a struct returned from 'NotifyFD.GetEvent'. 33 | type EventMetadata struct { 34 | unix.FanotifyEventMetadata 35 | } 36 | 37 | // GetPID return PID from event metadata. 38 | func (metadata *EventMetadata) GetPID() int { 39 | return int(metadata.Pid) 40 | } 41 | 42 | // Close is used to Close event Fd, use it to prevent Fd leak. 43 | func (metadata *EventMetadata) Close() error { 44 | if err := unix.Close(int(metadata.Fd)); err != nil { 45 | return fmt.Errorf("fanotify: failed to close Fd: %w", err) 46 | } 47 | 48 | return nil 49 | } 50 | 51 | // GetPath returns path to file for FD inside event metadata. 52 | func (metadata *EventMetadata) GetPath() (string, error) { 53 | path, err := os.Readlink( 54 | filepath.Join( 55 | ProcFsFd, 56 | strconv.FormatUint( 57 | uint64(metadata.Fd), 58 | 10, 59 | ), 60 | ), 61 | ) 62 | if err != nil { 63 | return "", fmt.Errorf("fanotify: %w", err) 64 | } 65 | 66 | return path, nil 67 | } 68 | 69 | // GetFdInfo returns parsed '/proc/self/fdinfo/%d' data. 70 | func (metadata *EventMetadata) GetFdInfo() (FdInfo, error) { 71 | var out FdInfo 72 | 73 | content, err := ioutil.ReadFile( 74 | filepath.Join( 75 | ProcFsFdInfo, 76 | strconv.FormatUint( 77 | uint64(metadata.Fd), 78 | 10, 79 | ), 80 | ), 81 | ) 82 | if err != nil { 83 | return out, fmt.Errorf("cnotifyd: procfs error, %w", err) 84 | } 85 | 86 | scanner := bufio.NewScanner(bytes.NewReader(content)) 87 | 88 | for scanner.Scan() { 89 | s := scanner.Text() 90 | 91 | var i int64 92 | 93 | switch { 94 | case strings.HasPrefix(s, "pos:"): 95 | if i, err = strconv.ParseInt( 96 | strings.TrimSpace(strings.TrimPrefix(s, "pos:")), 10, 32, 97 | ); err != nil { 98 | return out, fmt.Errorf("cnotifyd: procfs error, %w", err) 99 | } 100 | 101 | out.Position = int(i) 102 | case strings.HasPrefix(s, "flags:"): 103 | if i, err = strconv.ParseInt( 104 | strings.TrimSpace(strings.TrimPrefix(s, "flags:")), 8, 32, 105 | ); err != nil { 106 | return out, fmt.Errorf("cnotifyd: procfs error, %w", err) 107 | } 108 | 109 | out.Flags = int(i) 110 | case strings.HasPrefix(s, "mnt_id:"): 111 | if i, err = strconv.ParseInt( 112 | strings.TrimSpace(strings.TrimPrefix(s, "mnt_id:")), 10, 32, 113 | ); err != nil { 114 | return out, fmt.Errorf("cnotifyd: procfs error, %w", err) 115 | } 116 | 117 | out.MountID = int(i) 118 | } 119 | } 120 | 121 | if err := scanner.Err(); err != nil { 122 | return out, fmt.Errorf("cnotifyd: procfs error, %w", err) 123 | } 124 | 125 | return out, nil 126 | } 127 | 128 | // MatchMask returns 'true' when event metadata matches specified mask. 129 | func (metadata *EventMetadata) MatchMask(mask int) bool { 130 | return (metadata.Mask & uint64(mask)) == uint64(mask) 131 | } 132 | 133 | // File returns pointer to os.File created from event metadata supplied Fd. 134 | // File needs to be Closed after usage. 135 | func (metadata *EventMetadata) File() *os.File { 136 | // The fd used in os.NewFile() can be garbage collected, making the fd 137 | // used to create it invalid. This can be problematic, as now the fd can 138 | // be closed when the os.File created here is GC/Close() or when our 139 | // function Close() is used too. 140 | // 141 | // To avoid having so many references to the same fd and have one close 142 | // silently invalidate other users, we dup() the fd. This way, a new fd 143 | // is created every time File() is used and this even works if File() is 144 | // used multiple times (they never point to the same fd). 145 | // 146 | // For more details on when this can happen, see: 147 | // https://pkg.go.dev/os#File.Fd, that is referenced from: 148 | // https://pkg.go.dev/os#NewFile 149 | fd, err := unix.Dup(int(metadata.Fd)) 150 | if err != nil { 151 | return nil 152 | } 153 | 154 | return os.NewFile(uintptr(fd), "") 155 | } 156 | 157 | // NotifyFD is a notify file handle, used by all fanotify functions. 158 | type NotifyFD struct { 159 | Fd int 160 | File *os.File 161 | Rd io.Reader 162 | } 163 | 164 | // Initialize initializes the fanotify support. 165 | func Initialize(fanotifyFlags uint, openFlags int) (*NotifyFD, error) { 166 | fd, err := unix.FanotifyInit(fanotifyFlags, uint(openFlags)) 167 | if err != nil { 168 | return nil, fmt.Errorf("fanotify: init error, %w", err) 169 | } 170 | 171 | file := os.NewFile(uintptr(fd), "") 172 | rd := bufio.NewReader(file) 173 | 174 | return &NotifyFD{ 175 | Fd: fd, 176 | File: file, 177 | Rd: rd, 178 | }, err 179 | } 180 | 181 | // Mark implements Add/Delete/Modify for a fanotify mark. 182 | func (handle *NotifyFD) Mark(flags uint, mask uint64, dirFd int, path string) error { 183 | if err := unix.FanotifyMark(handle.Fd, flags, mask, dirFd, path); err != nil { 184 | return fmt.Errorf("fanotify: mark error, %w", err) 185 | } 186 | 187 | return nil 188 | } 189 | 190 | // GetEvent returns an event from the fanotify handle. 191 | func (handle *NotifyFD) GetEvent(skipPIDs ...int) (*EventMetadata, error) { 192 | event := new(EventMetadata) 193 | 194 | if err := binary.Read(handle.Rd, binary.LittleEndian, event); err != nil { 195 | return nil, fmt.Errorf("fanotify: event error, %w", err) 196 | } 197 | 198 | if event.Vers != unix.FANOTIFY_METADATA_VERSION { 199 | if err := event.Close(); err != nil { 200 | return nil, err 201 | } 202 | 203 | return nil, fmt.Errorf("fanotify: wrong metadata version") 204 | } 205 | 206 | for i := range skipPIDs { 207 | if int(event.Pid) == skipPIDs[i] { 208 | return nil, event.Close() 209 | } 210 | } 211 | 212 | return event, nil 213 | } 214 | 215 | // ResponseAllow sends an allow message back to fanotify, used for permission checks. 216 | func (handle *NotifyFD) ResponseAllow(ev *EventMetadata) error { 217 | if err := binary.Write( 218 | handle.File, 219 | binary.LittleEndian, 220 | &unix.FanotifyResponse{ 221 | Fd: ev.Fd, 222 | Response: unix.FAN_ALLOW, 223 | }, 224 | ); err != nil { 225 | return fmt.Errorf("fanotify: response error, %w", err) 226 | } 227 | 228 | return nil 229 | } 230 | 231 | // ResponseDeny sends a deny message back to fanotify, used for permission checks. 232 | func (handle *NotifyFD) ResponseDeny(ev *EventMetadata) error { 233 | if err := binary.Write( 234 | handle.File, 235 | binary.LittleEndian, 236 | &unix.FanotifyResponse{ 237 | Fd: ev.Fd, 238 | Response: unix.FAN_DENY, 239 | }, 240 | ); err != nil { 241 | return fmt.Errorf("fanotify: response error, %w", err) 242 | } 243 | 244 | return nil 245 | } 246 | -------------------------------------------------------------------------------- /fanotify/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/s3rj1k/go-fanotify/fanotify 2 | 3 | go 1.18 4 | 5 | require golang.org/x/sys v0.17.0 6 | -------------------------------------------------------------------------------- /fanotify/go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= 2 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 3 | --------------------------------------------------------------------------------