]>
Commit | Line | Data |
---|---|---|
e7145211 | 1 | /* SPDX-License-Identifier: GPL-2.0+ */ |
ea733a2f | 2 | |
07630cea LP |
3 | #include <errno.h> |
4 | #include <fcntl.h> | |
5 | #include <stdbool.h> | |
6 | #include <stddef.h> | |
32ff5bca | 7 | #include <sys/stat.h> |
07630cea | 8 | #include <unistd.h> |
ea733a2f | 9 | |
a2554ace | 10 | #include "alloc-util.h" |
c67f84b0 | 11 | #include "device-nodes.h" |
a2554ace YW |
12 | #include "device-private.h" |
13 | #include "device-util.h" | |
8fb3f009 | 14 | #include "dirent-util.h" |
a2554ace | 15 | #include "fd-util.h" |
f97b34a6 | 16 | #include "format-util.h" |
f4f15635 | 17 | #include "fs-util.h" |
5ea78a39 YW |
18 | #include "libudev-util.h" |
19 | #include "mkdir.h" | |
a2554ace | 20 | #include "path-util.h" |
07630cea LP |
21 | #include "selinux-util.h" |
22 | #include "smack-util.h" | |
d054f0a4 | 23 | #include "stdio-util.h" |
07630cea | 24 | #include "string-util.h" |
5ea78a39 | 25 | #include "strxcpyx.h" |
a2554ace | 26 | #include "udev-node.h" |
3708c0f4 | 27 | #include "user-util.h" |
9825617b | 28 | |
a2554ace | 29 | static int node_symlink(sd_device *dev, const char *node, const char *slink) { |
c1ee2674 | 30 | _cleanup_free_ char *slink_dirname = NULL, *target = NULL; |
a2554ace | 31 | const char *id_filename, *slink_tmp; |
c1ee2674 | 32 | struct stat stats; |
a2554ace YW |
33 | int r; |
34 | ||
35 | assert(dev); | |
36 | assert(node); | |
37 | assert(slink); | |
c1ee2674 YW |
38 | |
39 | slink_dirname = dirname_malloc(slink); | |
40 | if (!slink_dirname) | |
41 | return log_oom(); | |
912541b0 KS |
42 | |
43 | /* use relative link */ | |
c1ee2674 YW |
44 | r = path_make_relative(slink_dirname, node, &target); |
45 | if (r < 0) | |
e0ca42e3 | 46 | return log_device_error_errno(dev, r, "Failed to get relative path from '%s' to '%s': %m", slink, node); |
912541b0 KS |
47 | |
48 | /* preserve link with correct target, do not replace node of other device */ | |
49 | if (lstat(slink, &stats) == 0) { | |
9e791238 ZJS |
50 | if (S_ISBLK(stats.st_mode) || S_ISCHR(stats.st_mode)) |
51 | return log_device_error_errno(dev, SYNTHETIC_ERRNO(EOPNOTSUPP), | |
52 | "Conflicting device node '%s' found, link to '%s' will not be created.", slink, node); | |
53 | else if (S_ISLNK(stats.st_mode)) { | |
e7aa9512 | 54 | _cleanup_free_ char *buf = NULL; |
912541b0 | 55 | |
e7aa9512 YW |
56 | if (readlink_malloc(slink, &buf) >= 0 && |
57 | streq(target, buf)) { | |
e0ca42e3 | 58 | log_device_debug(dev, "Preserve already existing symlink '%s' to '%s'", slink, target); |
e7aa9512 YW |
59 | (void) label_fix(slink, LABEL_IGNORE_ENOENT); |
60 | (void) utimensat(AT_FDCWD, slink, NULL, AT_SYMLINK_NOFOLLOW); | |
61 | return 0; | |
912541b0 KS |
62 | } |
63 | } | |
64 | } else { | |
e0ca42e3 | 65 | log_device_debug(dev, "Creating symlink '%s' to '%s'", slink, target); |
912541b0 | 66 | do { |
a2554ace YW |
67 | r = mkdir_parents_label(slink, 0755); |
68 | if (!IN_SET(r, 0, -ENOENT)) | |
912541b0 | 69 | break; |
ecabcf8b | 70 | mac_selinux_create_file_prepare(slink, S_IFLNK); |
a2554ace YW |
71 | if (symlink(target, slink) < 0) |
72 | r = -errno; | |
ecabcf8b | 73 | mac_selinux_create_file_clear(); |
a2554ace YW |
74 | } while (r == -ENOENT); |
75 | if (r == 0) | |
76 | return 0; | |
6bee2065 YW |
77 | if (r < 0) |
78 | log_device_debug_errno(dev, r, "Failed to create symlink '%s' to '%s', trying to replace '%s': %m", slink, target, slink); | |
912541b0 KS |
79 | } |
80 | ||
e0ca42e3 | 81 | log_device_debug(dev, "Atomically replace '%s'", slink); |
a2554ace YW |
82 | r = device_get_id_filename(dev, &id_filename); |
83 | if (r < 0) | |
e0ca42e3 | 84 | return log_device_error_errno(dev, r, "Failed to get id_filename: %m"); |
a2554ace YW |
85 | slink_tmp = strjoina(slink, ".tmp-", id_filename); |
86 | (void) unlink(slink_tmp); | |
912541b0 | 87 | do { |
a2554ace YW |
88 | r = mkdir_parents_label(slink_tmp, 0755); |
89 | if (!IN_SET(r, 0, -ENOENT)) | |
912541b0 | 90 | break; |
ecabcf8b | 91 | mac_selinux_create_file_prepare(slink_tmp, S_IFLNK); |
a2554ace YW |
92 | if (symlink(target, slink_tmp) < 0) |
93 | r = -errno; | |
ecabcf8b | 94 | mac_selinux_create_file_clear(); |
a2554ace YW |
95 | } while (r == -ENOENT); |
96 | if (r < 0) | |
e0ca42e3 | 97 | return log_device_error_errno(dev, r, "Failed to create symlink '%s' to '%s': %m", slink_tmp, target); |
a2554ace YW |
98 | |
99 | if (rename(slink_tmp, slink) < 0) { | |
3fb10690 | 100 | r = log_device_error_errno(dev, errno, "Failed to rename '%s' to '%s': %m", slink_tmp, slink); |
a2554ace | 101 | (void) unlink(slink_tmp); |
912541b0 | 102 | } |
a2554ace YW |
103 | |
104 | return r; | |
fa33d857 KS |
105 | } |
106 | ||
6c29f2b9 | 107 | /* find device node of device with highest priority */ |
a2554ace YW |
108 | static int link_find_prioritized(sd_device *dev, bool add, const char *stackdir, char **ret) { |
109 | _cleanup_closedir_ DIR *dir = NULL; | |
110 | _cleanup_free_ char *target = NULL; | |
8fb3f009 | 111 | struct dirent *dent; |
a2554ace YW |
112 | int r, priority = 0; |
113 | ||
114 | assert(!add || dev); | |
115 | assert(stackdir); | |
116 | assert(ret); | |
912541b0 KS |
117 | |
118 | if (add) { | |
a2554ace YW |
119 | const char *devnode; |
120 | ||
121 | r = device_get_devlink_priority(dev, &priority); | |
122 | if (r < 0) | |
123 | return r; | |
124 | ||
125 | r = sd_device_get_devname(dev, &devnode); | |
126 | if (r < 0) | |
127 | return r; | |
128 | ||
129 | target = strdup(devnode); | |
130 | if (!target) | |
131 | return -ENOMEM; | |
912541b0 KS |
132 | } |
133 | ||
134 | dir = opendir(stackdir); | |
a2554ace YW |
135 | if (!dir) { |
136 | if (target) { | |
137 | *ret = TAKE_PTR(target); | |
138 | return 0; | |
139 | } | |
140 | ||
141 | return -errno; | |
142 | } | |
143 | ||
8fb3f009 | 144 | FOREACH_DIRENT_ALL(dent, dir, break) { |
a2554ace YW |
145 | _cleanup_(sd_device_unrefp) sd_device *dev_db = NULL; |
146 | const char *devnode, *id_filename; | |
147 | int db_prio = 0; | |
912541b0 | 148 | |
8fb3f009 | 149 | if (dent->d_name[0] == '\0') |
912541b0 KS |
150 | break; |
151 | if (dent->d_name[0] == '.') | |
152 | continue; | |
153 | ||
e0ca42e3 | 154 | log_device_debug(dev, "Found '%s' claiming '%s'", dent->d_name, stackdir); |
a2554ace YW |
155 | |
156 | if (device_get_id_filename(dev, &id_filename) < 0) | |
157 | continue; | |
912541b0 KS |
158 | |
159 | /* did we find ourself? */ | |
a2554ace | 160 | if (streq(dent->d_name, id_filename)) |
912541b0 KS |
161 | continue; |
162 | ||
a2554ace YW |
163 | if (sd_device_new_from_device_id(&dev_db, dent->d_name) < 0) |
164 | continue; | |
165 | ||
166 | if (sd_device_get_devname(dev_db, &devnode) < 0) | |
167 | continue; | |
168 | ||
169 | if (device_get_devlink_priority(dev_db, &db_prio) < 0) | |
170 | continue; | |
171 | ||
172 | if (target && db_prio <= priority) | |
173 | continue; | |
174 | ||
e0ca42e3 | 175 | log_device_debug(dev_db, "Device claims priority %i for '%s'", db_prio, stackdir); |
a2554ace YW |
176 | |
177 | r = free_and_strdup(&target, devnode); | |
178 | if (r < 0) | |
179 | return r; | |
180 | priority = db_prio; | |
912541b0 | 181 | } |
a2554ace | 182 | |
82d9ac23 YW |
183 | if (!target) |
184 | return -ENOENT; | |
185 | ||
a2554ace YW |
186 | *ret = TAKE_PTR(target); |
187 | return 0; | |
aa8734ff KS |
188 | } |
189 | ||
6c29f2b9 | 190 | /* manage "stack of names" with possibly specified device priorities */ |
a2554ace YW |
191 | static int link_update(sd_device *dev, const char *slink, bool add) { |
192 | _cleanup_free_ char *target = NULL, *filename = NULL, *dirname = NULL; | |
193 | char name_enc[PATH_MAX]; | |
194 | const char *id_filename; | |
195 | int r; | |
196 | ||
197 | assert(dev); | |
198 | assert(slink); | |
199 | ||
200 | r = device_get_id_filename(dev, &id_filename); | |
201 | if (r < 0) | |
e0ca42e3 | 202 | return log_device_debug_errno(dev, r, "Failed to get id_filename: %m"); |
912541b0 | 203 | |
fbd0b64f | 204 | util_path_encode(slink + STRLEN("/dev"), name_enc, sizeof(name_enc)); |
62a85ee0 | 205 | dirname = path_join("/run/udev/links/", name_enc); |
a2554ace YW |
206 | if (!dirname) |
207 | return log_oom(); | |
62a85ee0 | 208 | filename = path_join(dirname, id_filename); |
a2554ace YW |
209 | if (!filename) |
210 | return log_oom(); | |
912541b0 | 211 | |
baa30fbc | 212 | if (!add && unlink(filename) == 0) |
a2554ace | 213 | (void) rmdir(dirname); |
912541b0 | 214 | |
a2554ace YW |
215 | r = link_find_prioritized(dev, add, dirname, &target); |
216 | if (r < 0) { | |
e0ca42e3 | 217 | log_device_debug(dev, "No reference left, removing '%s'", slink); |
912541b0 | 218 | if (unlink(slink) == 0) |
a2554ace | 219 | (void) rmdir_parents(slink, "/"); |
a126a38a | 220 | } else |
a2554ace | 221 | (void) node_symlink(dev, target, slink); |
912541b0 | 222 | |
a2554ace | 223 | if (add) |
912541b0 | 224 | do { |
a2554ace | 225 | _cleanup_close_ int fd = -1; |
912541b0 | 226 | |
a2554ace YW |
227 | r = mkdir_parents(filename, 0755); |
228 | if (!IN_SET(r, 0, -ENOENT)) | |
912541b0 KS |
229 | break; |
230 | fd = open(filename, O_WRONLY|O_CREAT|O_CLOEXEC|O_TRUNC|O_NOFOLLOW, 0444); | |
a2554ace YW |
231 | if (fd < 0) |
232 | r = -errno; | |
233 | } while (r == -ENOENT); | |
234 | ||
235 | return r; | |
24f0605c KS |
236 | } |
237 | ||
a2554ace YW |
238 | int udev_node_update_old_links(sd_device *dev, sd_device *dev_old) { |
239 | const char *name, *devpath; | |
240 | int r; | |
241 | ||
242 | assert(dev); | |
243 | assert(dev_old); | |
244 | ||
245 | r = sd_device_get_devpath(dev, &devpath); | |
246 | if (r < 0) | |
e0ca42e3 | 247 | return log_device_debug_errno(dev, r, "Failed to get devpath: %m"); |
912541b0 KS |
248 | |
249 | /* update possible left-over symlinks */ | |
a2554ace YW |
250 | FOREACH_DEVICE_DEVLINK(dev_old, name) { |
251 | const char *name_current; | |
252 | bool found = false; | |
912541b0 KS |
253 | |
254 | /* check if old link name still belongs to this device */ | |
a2554ace | 255 | FOREACH_DEVICE_DEVLINK(dev, name_current) |
090be865 | 256 | if (streq(name, name_current)) { |
a2554ace | 257 | found = true; |
912541b0 KS |
258 | break; |
259 | } | |
a2554ace | 260 | |
912541b0 KS |
261 | if (found) |
262 | continue; | |
263 | ||
e0ca42e3 YW |
264 | log_device_debug(dev, "Updating old name, '%s' no longer belonging to '%s'", |
265 | name, devpath); | |
8a173387 | 266 | link_update(dev, name, false); |
912541b0 | 267 | } |
a2554ace YW |
268 | |
269 | return 0; | |
24f0605c KS |
270 | } |
271 | ||
3708c0f4 | 272 | static int node_permissions_apply(sd_device *dev, bool apply_mac, |
c26547d6 | 273 | mode_t mode, uid_t uid, gid_t gid, |
39a15c8a | 274 | OrderedHashmap *seclabel_list) { |
a2554ace | 275 | const char *devnode, *subsystem, *id_filename = NULL; |
a2554ace YW |
276 | struct stat stats; |
277 | dev_t devnum; | |
3708c0f4 ZJS |
278 | bool apply_mode, apply_uid, apply_gid; |
279 | int r; | |
a2554ace YW |
280 | |
281 | assert(dev); | |
282 | ||
283 | r = sd_device_get_devname(dev, &devnode); | |
284 | if (r < 0) | |
e0ca42e3 | 285 | return log_device_debug_errno(dev, r, "Failed to get devname: %m"); |
a2554ace YW |
286 | r = sd_device_get_subsystem(dev, &subsystem); |
287 | if (r < 0) | |
e0ca42e3 | 288 | return log_device_debug_errno(dev, r, "Failed to get subsystem: %m"); |
a2554ace YW |
289 | r = sd_device_get_devnum(dev, &devnum); |
290 | if (r < 0) | |
e0ca42e3 | 291 | return log_device_debug_errno(dev, r, "Failed to get devnum: %m"); |
a2554ace | 292 | (void) device_get_id_filename(dev, &id_filename); |
912541b0 | 293 | |
a2554ace | 294 | if (streq(subsystem, "block")) |
912541b0 KS |
295 | mode |= S_IFBLK; |
296 | else | |
297 | mode |= S_IFCHR; | |
298 | ||
b64b83d1 YW |
299 | if (lstat(devnode, &stats) < 0) { |
300 | if (errno == ENOENT) | |
301 | return 0; /* this is necessarily racey, so ignore missing the device */ | |
ab3a7372 | 302 | return log_device_debug_errno(dev, errno, "cannot stat() node %s: %m", devnode); |
b64b83d1 | 303 | } |
912541b0 | 304 | |
3708c0f4 ZJS |
305 | if ((mode != MODE_INVALID && (stats.st_mode & S_IFMT) != (mode & S_IFMT)) || stats.st_rdev != devnum) |
306 | return log_device_debug_errno(dev, SYNTHETIC_ERRNO(EEXIST), | |
307 | "Found node '%s' with non-matching devnum %s, skip handling", | |
e0ca42e3 | 308 | devnode, id_filename); |
912541b0 | 309 | |
3708c0f4 ZJS |
310 | apply_mode = mode != MODE_INVALID && (stats.st_mode & 0777) != (mode & 0777); |
311 | apply_uid = uid_is_valid(uid) && stats.st_uid != uid; | |
312 | apply_gid = gid_is_valid(gid) && stats.st_gid != gid; | |
313 | ||
314 | if (apply_mode || apply_uid || apply_gid || apply_mac) { | |
a2554ace | 315 | bool selinux = false, smack = false; |
d838e145 YW |
316 | const char *name, *label; |
317 | Iterator i; | |
b7e2b764 | 318 | |
3708c0f4 | 319 | if (apply_mode || apply_uid || apply_gid) { |
20f45f4b YW |
320 | log_device_debug(dev, "Setting permissions %s, uid=" UID_FMT ", gid=" GID_FMT ", mode=%#o", |
321 | devnode, | |
322 | uid_is_valid(uid) ? uid : stats.st_uid, | |
323 | gid_is_valid(gid) ? gid : stats.st_gid, | |
324 | mode != MODE_INVALID ? mode & 0777 : stats.st_mode & 0777); | |
4b3b5bc7 LP |
325 | |
326 | r = chmod_and_chown(devnode, mode, uid, gid); | |
327 | if (r < 0) | |
b64b83d1 YW |
328 | log_device_full(dev, r == -ENOENT ? LOG_DEBUG : LOG_ERR, r, |
329 | "Failed to set owner/mode of %s to uid=" UID_FMT | |
330 | ", gid=" GID_FMT ", mode=%#o: %m", | |
331 | devnode, | |
332 | uid_is_valid(uid) ? uid : stats.st_uid, | |
333 | gid_is_valid(gid) ? gid : stats.st_gid, | |
334 | mode != MODE_INVALID ? mode & 0777 : stats.st_mode & 0777); | |
a2554ace | 335 | } else |
20f45f4b YW |
336 | log_device_debug(dev, "Preserve permissions of %s, uid=" UID_FMT ", gid=" GID_FMT ", mode=%#o", |
337 | devnode, | |
338 | uid_is_valid(uid) ? uid : stats.st_uid, | |
339 | gid_is_valid(gid) ? gid : stats.st_gid, | |
340 | mode != MODE_INVALID ? mode & 0777 : stats.st_mode & 0777); | |
c26547d6 | 341 | |
c26547d6 | 342 | /* apply SECLABEL{$module}=$label */ |
39a15c8a | 343 | ORDERED_HASHMAP_FOREACH_KEY(label, name, seclabel_list, i) { |
a2554ace | 344 | int q; |
c26547d6 | 345 | |
c26547d6 | 346 | if (streq(name, "selinux")) { |
b7e2b764 | 347 | selinux = true; |
d53e386d | 348 | |
a2554ace YW |
349 | q = mac_selinux_apply(devnode, label); |
350 | if (q < 0) | |
b64b83d1 YW |
351 | log_device_full(dev, q == -ENOENT ? LOG_DEBUG : LOG_ERR, q, |
352 | "SECLABEL: failed to set SELinux label '%s': %m", label); | |
463b5dbb | 353 | else |
e0ca42e3 | 354 | log_device_debug(dev, "SECLABEL: set SELinux label '%s'", label); |
c26547d6 | 355 | |
9a4e038c | 356 | } else if (streq(name, "smack")) { |
b7e2b764 | 357 | smack = true; |
d53e386d | 358 | |
a2554ace YW |
359 | q = mac_smack_apply(devnode, SMACK_ATTR_ACCESS, label); |
360 | if (q < 0) | |
b64b83d1 YW |
361 | log_device_full(dev, q == -ENOENT ? LOG_DEBUG : LOG_ERR, q, |
362 | "SECLABEL: failed to set SMACK label '%s': %m", label); | |
c26547d6 | 363 | else |
e0ca42e3 | 364 | log_device_debug(dev, "SECLABEL: set SMACK label '%s'", label); |
c26547d6 KS |
365 | |
366 | } else | |
e0ca42e3 | 367 | log_device_error(dev, "SECLABEL: unknown subsystem, ignoring '%s'='%s'", name, label); |
c26547d6 | 368 | } |
b7e2b764 KS |
369 | |
370 | /* set the defaults */ | |
371 | if (!selinux) | |
08c84981 | 372 | (void) mac_selinux_fix(devnode, LABEL_IGNORE_ENOENT); |
9a4e038c | 373 | if (!smack) |
a2554ace | 374 | (void) mac_smack_apply(devnode, SMACK_ATTR_ACCESS, NULL); |
48a849ee | 375 | } |
912541b0 KS |
376 | |
377 | /* always update timestamp when we re-use the node, like on media change events */ | |
a2554ace YW |
378 | (void) utimensat(AT_FDCWD, devnode, NULL, 0); |
379 | ||
380 | return r; | |
220893b3 KS |
381 | } |
382 | ||
a2554ace YW |
383 | static int xsprintf_dev_num_path_from_sd_device(sd_device *dev, char **ret) { |
384 | char filename[DEV_NUM_PATH_MAX], *s; | |
385 | const char *subsystem; | |
386 | dev_t devnum; | |
387 | int r; | |
388 | ||
389 | assert(ret); | |
912541b0 | 390 | |
a2554ace YW |
391 | r = sd_device_get_subsystem(dev, &subsystem); |
392 | if (r < 0) | |
393 | return r; | |
912541b0 | 394 | |
a2554ace YW |
395 | r = sd_device_get_devnum(dev, &devnum); |
396 | if (r < 0) | |
397 | return r; | |
912541b0 | 398 | |
c67f84b0 | 399 | xsprintf_dev_num_path(filename, |
a2554ace YW |
400 | streq(subsystem, "block") ? "block" : "char", |
401 | devnum); | |
402 | ||
403 | s = strdup(filename); | |
404 | if (!s) | |
405 | return -ENOMEM; | |
406 | ||
407 | *ret = s; | |
408 | return 0; | |
409 | } | |
410 | ||
411 | int udev_node_add(sd_device *dev, bool apply, | |
412 | mode_t mode, uid_t uid, gid_t gid, | |
39a15c8a | 413 | OrderedHashmap *seclabel_list) { |
a2554ace YW |
414 | const char *devnode, *devlink; |
415 | _cleanup_free_ char *filename = NULL; | |
416 | int r; | |
417 | ||
418 | assert(dev); | |
419 | ||
420 | r = sd_device_get_devname(dev, &devnode); | |
421 | if (r < 0) | |
e0ca42e3 | 422 | return log_device_debug_errno(dev, r, "Failed to get devnode: %m"); |
a2554ace YW |
423 | |
424 | if (DEBUG_LOGGING) { | |
425 | const char *id_filename = NULL; | |
426 | ||
427 | (void) device_get_id_filename(dev, &id_filename); | |
20f45f4b | 428 | log_device_debug(dev, "Handling device node '%s', devnum=%s", devnode, strnull(id_filename)); |
a2554ace YW |
429 | } |
430 | ||
431 | r = node_permissions_apply(dev, apply, mode, uid, gid, seclabel_list); | |
432 | if (r < 0) | |
433 | return r; | |
434 | ||
435 | r = xsprintf_dev_num_path_from_sd_device(dev, &filename); | |
436 | if (r < 0) | |
e0ca42e3 | 437 | return log_device_debug_errno(dev, r, "Failed to get device path: %m"); |
a2554ace YW |
438 | |
439 | /* always add /dev/{block,char}/$major:$minor */ | |
440 | (void) node_symlink(dev, devnode, filename); | |
912541b0 KS |
441 | |
442 | /* create/update symlinks, add symlinks to name index */ | |
a2554ace YW |
443 | FOREACH_DEVICE_DEVLINK(dev, devlink) |
444 | (void) link_update(dev, devlink, true); | |
445 | ||
446 | return 0; | |
ea733a2f GKH |
447 | } |
448 | ||
a2554ace YW |
449 | int udev_node_remove(sd_device *dev) { |
450 | _cleanup_free_ char *filename = NULL; | |
451 | const char *devlink; | |
452 | int r; | |
453 | ||
454 | assert(dev); | |
912541b0 KS |
455 | |
456 | /* remove/update symlinks, remove symlinks from name index */ | |
a2554ace YW |
457 | FOREACH_DEVICE_DEVLINK(dev, devlink) |
458 | (void) link_update(dev, devlink, false); | |
459 | ||
460 | r = xsprintf_dev_num_path_from_sd_device(dev, &filename); | |
461 | if (r < 0) | |
e0ca42e3 | 462 | return log_device_debug_errno(dev, r, "Failed to get device path: %m"); |
912541b0 KS |
463 | |
464 | /* remove /dev/{block,char}/$major:$minor */ | |
a2554ace YW |
465 | (void) unlink(filename); |
466 | ||
467 | return 0; | |
ea733a2f | 468 | } |