| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * Copyright (C) 2017-2023 Oracle. All Rights Reserved. |
| * Author: Darrick J. Wong <djwong@kernel.org> |
| */ |
| #include "xfs.h" |
| #include "xfs_fs.h" |
| #include "xfs_shared.h" |
| #include "xfs_format.h" |
| #include "xfs_trans_resv.h" |
| #include "xfs_mount.h" |
| #include "xfs_log_format.h" |
| #include "xfs_trans.h" |
| #include "xfs_inode.h" |
| #include "xfs_quota.h" |
| #include "xfs_qm.h" |
| #include "xfs_errortag.h" |
| #include "xfs_error.h" |
| #include "xfs_scrub.h" |
| #include "scrub/scrub.h" |
| #include "scrub/common.h" |
| #include "scrub/trace.h" |
| #include "scrub/repair.h" |
| #include "scrub/health.h" |
| #include "scrub/stats.h" |
| #include "scrub/xfile.h" |
| |
| /* |
| * Online Scrub and Repair |
| * |
| * Traditionally, XFS (the kernel driver) did not know how to check or |
| * repair on-disk data structures. That task was left to the xfs_check |
| * and xfs_repair tools, both of which require taking the filesystem |
| * offline for a thorough but time consuming examination. Online |
| * scrub & repair, on the other hand, enables us to check the metadata |
| * for obvious errors while carefully stepping around the filesystem's |
| * ongoing operations, locking rules, etc. |
| * |
| * Given that most XFS metadata consist of records stored in a btree, |
| * most of the checking functions iterate the btree blocks themselves |
| * looking for irregularities. When a record block is encountered, each |
| * record can be checked for obviously bad values. Record values can |
| * also be cross-referenced against other btrees to look for potential |
| * misunderstandings between pieces of metadata. |
| * |
| * It is expected that the checkers responsible for per-AG metadata |
| * structures will lock the AG headers (AGI, AGF, AGFL), iterate the |
| * metadata structure, and perform any relevant cross-referencing before |
| * unlocking the AG and returning the results to userspace. These |
| * scrubbers must not keep an AG locked for too long to avoid tying up |
| * the block and inode allocators. |
| * |
| * Block maps and b-trees rooted in an inode present a special challenge |
| * because they can involve extents from any AG. The general scrubber |
| * structure of lock -> check -> xref -> unlock still holds, but AG |
| * locking order rules /must/ be obeyed to avoid deadlocks. The |
| * ordering rule, of course, is that we must lock in increasing AG |
| * order. Helper functions are provided to track which AG headers we've |
| * already locked. If we detect an imminent locking order violation, we |
| * can signal a potential deadlock, in which case the scrubber can jump |
| * out to the top level, lock all the AGs in order, and retry the scrub. |
| * |
| * For file data (directories, extended attributes, symlinks) scrub, we |
| * can simply lock the inode and walk the data. For btree data |
| * (directories and attributes) we follow the same btree-scrubbing |
| * strategy outlined previously to check the records. |
| * |
| * We use a bit of trickery with transactions to avoid buffer deadlocks |
| * if there is a cycle in the metadata. The basic problem is that |
| * travelling down a btree involves locking the current buffer at each |
| * tree level. If a pointer should somehow point back to a buffer that |
| * we've already examined, we will deadlock due to the second buffer |
| * locking attempt. Note however that grabbing a buffer in transaction |
| * context links the locked buffer to the transaction. If we try to |
| * re-grab the buffer in the context of the same transaction, we avoid |
| * the second lock attempt and continue. Between the verifier and the |
| * scrubber, something will notice that something is amiss and report |
| * the corruption. Therefore, each scrubber will allocate an empty |
| * transaction, attach buffers to it, and cancel the transaction at the |
| * end of the scrub run. Cancelling a non-dirty transaction simply |
| * unlocks the buffers. |
| * |
| * There are four pieces of data that scrub can communicate to |
| * userspace. The first is the error code (errno), which can be used to |
| * communicate operational errors in performing the scrub. There are |
| * also three flags that can be set in the scrub context. If the data |
| * structure itself is corrupt, the CORRUPT flag will be set. If |
| * the metadata is correct but otherwise suboptimal, the PREEN flag |
| * will be set. |
| * |
| * We perform secondary validation of filesystem metadata by |
| * cross-referencing every record with all other available metadata. |
| * For example, for block mapping extents, we verify that there are no |
| * records in the free space and inode btrees corresponding to that |
| * space extent and that there is a corresponding entry in the reverse |
| * mapping btree. Inconsistent metadata is noted by setting the |
| * XCORRUPT flag; btree query function errors are noted by setting the |
| * XFAIL flag and deleting the cursor to prevent further attempts to |
| * cross-reference with a defective btree. |
| * |
| * If a piece of metadata proves corrupt or suboptimal, the userspace |
| * program can ask the kernel to apply some tender loving care (TLC) to |
| * the metadata object by setting the REPAIR flag and re-calling the |
| * scrub ioctl. "Corruption" is defined by metadata violating the |
| * on-disk specification; operations cannot continue if the violation is |
| * left untreated. It is possible for XFS to continue if an object is |
| * "suboptimal", however performance may be degraded. Repairs are |
| * usually performed by rebuilding the metadata entirely out of |
| * redundant metadata. Optimizing, on the other hand, can sometimes be |
| * done without rebuilding entire structures. |
| * |
| * Generally speaking, the repair code has the following code structure: |
| * Lock -> scrub -> repair -> commit -> re-lock -> re-scrub -> unlock. |
| * The first check helps us figure out if we need to rebuild or simply |
| * optimize the structure so that the rebuild knows what to do. The |
| * second check evaluates the completeness of the repair; that is what |
| * is reported to userspace. |
| * |
| * A quick note on symbol prefixes: |
| * - "xfs_" are general XFS symbols. |
| * - "xchk_" are symbols related to metadata checking. |
| * - "xrep_" are symbols related to metadata repair. |
| * - "xfs_scrub_" are symbols that tie online fsck to the rest of XFS. |
| */ |
| |
| /* |
| * Scrub probe -- userspace uses this to probe if we're willing to scrub |
| * or repair a given mountpoint. This will be used by xfs_scrub to |
| * probe the kernel's abilities to scrub (and repair) the metadata. We |
| * do this by validating the ioctl inputs from userspace, preparing the |
| * filesystem for a scrub (or a repair) operation, and immediately |
| * returning to userspace. Userspace can use the returned errno and |
| * structure state to decide (in broad terms) if scrub/repair are |
| * supported by the running kernel. |
| */ |
| static int |
| xchk_probe( |
| struct xfs_scrub *sc) |
| { |
| int error = 0; |
| |
| if (xchk_should_terminate(sc, &error)) |
| return error; |
| |
| return 0; |
| } |
| |
| /* Scrub setup and teardown */ |
| |
| static inline void |
| xchk_fsgates_disable( |
| struct xfs_scrub *sc) |
| { |
| if (!(sc->flags & XCHK_FSGATES_ALL)) |
| return; |
| |
| trace_xchk_fsgates_disable(sc, sc->flags & XCHK_FSGATES_ALL); |
| |
| if (sc->flags & XCHK_FSGATES_DRAIN) |
| xfs_drain_wait_disable(); |
| |
| sc->flags &= ~XCHK_FSGATES_ALL; |
| } |
| |
| /* Free all the resources and finish the transactions. */ |
| STATIC int |
| xchk_teardown( |
| struct xfs_scrub *sc, |
| int error) |
| { |
| xchk_ag_free(sc, &sc->sa); |
| if (sc->tp) { |
| if (error == 0 && (sc->sm->sm_flags & XFS_SCRUB_IFLAG_REPAIR)) |
| error = xfs_trans_commit(sc->tp); |
| else |
| xfs_trans_cancel(sc->tp); |
| sc->tp = NULL; |
| } |
| if (sc->ip) { |
| if (sc->ilock_flags) |
| xchk_iunlock(sc, sc->ilock_flags); |
| xchk_irele(sc, sc->ip); |
| sc->ip = NULL; |
| } |
| if (sc->flags & XCHK_HAVE_FREEZE_PROT) { |
| sc->flags &= ~XCHK_HAVE_FREEZE_PROT; |
| mnt_drop_write_file(sc->file); |
| } |
| if (sc->xfile) { |
| xfile_destroy(sc->xfile); |
| sc->xfile = NULL; |
| } |
| if (sc->buf) { |
| if (sc->buf_cleanup) |
| sc->buf_cleanup(sc->buf); |
| kvfree(sc->buf); |
| sc->buf_cleanup = NULL; |
| sc->buf = NULL; |
| } |
| |
| xchk_fsgates_disable(sc); |
| return error; |
| } |
| |
| /* Scrubbing dispatch. */ |
| |
| static const struct xchk_meta_ops meta_scrub_ops[] = { |
| [XFS_SCRUB_TYPE_PROBE] = { /* ioctl presence test */ |
| .type = ST_NONE, |
| .setup = xchk_setup_fs, |
| .scrub = xchk_probe, |
| .repair = xrep_probe, |
| }, |
| [XFS_SCRUB_TYPE_SB] = { /* superblock */ |
| .type = ST_PERAG, |
| .setup = xchk_setup_agheader, |
| .scrub = xchk_superblock, |
| .repair = xrep_superblock, |
| }, |
| [XFS_SCRUB_TYPE_AGF] = { /* agf */ |
| .type = ST_PERAG, |
| .setup = xchk_setup_agheader, |
| .scrub = xchk_agf, |
| .repair = xrep_agf, |
| }, |
| [XFS_SCRUB_TYPE_AGFL]= { /* agfl */ |
| .type = ST_PERAG, |
| .setup = xchk_setup_agheader, |
| .scrub = xchk_agfl, |
| .repair = xrep_agfl, |
| }, |
| [XFS_SCRUB_TYPE_AGI] = { /* agi */ |
| .type = ST_PERAG, |
| .setup = xchk_setup_agheader, |
| .scrub = xchk_agi, |
| .repair = xrep_agi, |
| }, |
| [XFS_SCRUB_TYPE_BNOBT] = { /* bnobt */ |
| .type = ST_PERAG, |
| .setup = xchk_setup_ag_allocbt, |
| .scrub = xchk_bnobt, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_CNTBT] = { /* cntbt */ |
| .type = ST_PERAG, |
| .setup = xchk_setup_ag_allocbt, |
| .scrub = xchk_cntbt, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_INOBT] = { /* inobt */ |
| .type = ST_PERAG, |
| .setup = xchk_setup_ag_iallocbt, |
| .scrub = xchk_inobt, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_FINOBT] = { /* finobt */ |
| .type = ST_PERAG, |
| .setup = xchk_setup_ag_iallocbt, |
| .scrub = xchk_finobt, |
| .has = xfs_has_finobt, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_RMAPBT] = { /* rmapbt */ |
| .type = ST_PERAG, |
| .setup = xchk_setup_ag_rmapbt, |
| .scrub = xchk_rmapbt, |
| .has = xfs_has_rmapbt, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_REFCNTBT] = { /* refcountbt */ |
| .type = ST_PERAG, |
| .setup = xchk_setup_ag_refcountbt, |
| .scrub = xchk_refcountbt, |
| .has = xfs_has_reflink, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_INODE] = { /* inode record */ |
| .type = ST_INODE, |
| .setup = xchk_setup_inode, |
| .scrub = xchk_inode, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_BMBTD] = { /* inode data fork */ |
| .type = ST_INODE, |
| .setup = xchk_setup_inode_bmap, |
| .scrub = xchk_bmap_data, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_BMBTA] = { /* inode attr fork */ |
| .type = ST_INODE, |
| .setup = xchk_setup_inode_bmap, |
| .scrub = xchk_bmap_attr, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_BMBTC] = { /* inode CoW fork */ |
| .type = ST_INODE, |
| .setup = xchk_setup_inode_bmap, |
| .scrub = xchk_bmap_cow, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_DIR] = { /* directory */ |
| .type = ST_INODE, |
| .setup = xchk_setup_directory, |
| .scrub = xchk_directory, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_XATTR] = { /* extended attributes */ |
| .type = ST_INODE, |
| .setup = xchk_setup_xattr, |
| .scrub = xchk_xattr, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_SYMLINK] = { /* symbolic link */ |
| .type = ST_INODE, |
| .setup = xchk_setup_symlink, |
| .scrub = xchk_symlink, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_PARENT] = { /* parent pointers */ |
| .type = ST_INODE, |
| .setup = xchk_setup_parent, |
| .scrub = xchk_parent, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_RTBITMAP] = { /* realtime bitmap */ |
| .type = ST_FS, |
| .setup = xchk_setup_rtbitmap, |
| .scrub = xchk_rtbitmap, |
| .has = xfs_has_realtime, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_RTSUM] = { /* realtime summary */ |
| .type = ST_FS, |
| .setup = xchk_setup_rtsummary, |
| .scrub = xchk_rtsummary, |
| .has = xfs_has_realtime, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_UQUOTA] = { /* user quota */ |
| .type = ST_FS, |
| .setup = xchk_setup_quota, |
| .scrub = xchk_quota, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_GQUOTA] = { /* group quota */ |
| .type = ST_FS, |
| .setup = xchk_setup_quota, |
| .scrub = xchk_quota, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_PQUOTA] = { /* project quota */ |
| .type = ST_FS, |
| .setup = xchk_setup_quota, |
| .scrub = xchk_quota, |
| .repair = xrep_notsupported, |
| }, |
| [XFS_SCRUB_TYPE_FSCOUNTERS] = { /* fs summary counters */ |
| .type = ST_FS, |
| .setup = xchk_setup_fscounters, |
| .scrub = xchk_fscounters, |
| .repair = xrep_notsupported, |
| }, |
| }; |
| |
| static int |
| xchk_validate_inputs( |
| struct xfs_mount *mp, |
| struct xfs_scrub_metadata *sm) |
| { |
| int error; |
| const struct xchk_meta_ops *ops; |
| |
| error = -EINVAL; |
| /* Check our inputs. */ |
| sm->sm_flags &= ~XFS_SCRUB_FLAGS_OUT; |
| if (sm->sm_flags & ~XFS_SCRUB_FLAGS_IN) |
| goto out; |
| /* sm_reserved[] must be zero */ |
| if (memchr_inv(sm->sm_reserved, 0, sizeof(sm->sm_reserved))) |
| goto out; |
| |
| error = -ENOENT; |
| /* Do we know about this type of metadata? */ |
| if (sm->sm_type >= XFS_SCRUB_TYPE_NR) |
| goto out; |
| ops = &meta_scrub_ops[sm->sm_type]; |
| if (ops->setup == NULL || ops->scrub == NULL) |
| goto out; |
| /* Does this fs even support this type of metadata? */ |
| if (ops->has && !ops->has(mp)) |
| goto out; |
| |
| error = -EINVAL; |
| /* restricting fields must be appropriate for type */ |
| switch (ops->type) { |
| case ST_NONE: |
| case ST_FS: |
| if (sm->sm_ino || sm->sm_gen || sm->sm_agno) |
| goto out; |
| break; |
| case ST_PERAG: |
| if (sm->sm_ino || sm->sm_gen || |
| sm->sm_agno >= mp->m_sb.sb_agcount) |
| goto out; |
| break; |
| case ST_INODE: |
| if (sm->sm_agno || (sm->sm_gen && !sm->sm_ino)) |
| goto out; |
| break; |
| default: |
| goto out; |
| } |
| |
| /* No rebuild without repair. */ |
| if ((sm->sm_flags & XFS_SCRUB_IFLAG_FORCE_REBUILD) && |
| !(sm->sm_flags & XFS_SCRUB_IFLAG_REPAIR)) |
| return -EINVAL; |
| |
| /* |
| * We only want to repair read-write v5+ filesystems. Defer the check |
| * for ops->repair until after our scrub confirms that we need to |
| * perform repairs so that we avoid failing due to not supporting |
| * repairing an object that doesn't need repairs. |
| */ |
| if (sm->sm_flags & XFS_SCRUB_IFLAG_REPAIR) { |
| error = -EOPNOTSUPP; |
| if (!xfs_has_crc(mp)) |
| goto out; |
| |
| error = -EROFS; |
| if (xfs_is_readonly(mp)) |
| goto out; |
| } |
| |
| error = 0; |
| out: |
| return error; |
| } |
| |
| #ifdef CONFIG_XFS_ONLINE_REPAIR |
| static inline void xchk_postmortem(struct xfs_scrub *sc) |
| { |
| /* |
| * Userspace asked us to repair something, we repaired it, rescanned |
| * it, and the rescan says it's still broken. Scream about this in |
| * the system logs. |
| */ |
| if ((sc->sm->sm_flags & XFS_SCRUB_IFLAG_REPAIR) && |
| (sc->sm->sm_flags & (XFS_SCRUB_OFLAG_CORRUPT | |
| XFS_SCRUB_OFLAG_XCORRUPT))) |
| xrep_failure(sc->mp); |
| } |
| #else |
| static inline void xchk_postmortem(struct xfs_scrub *sc) |
| { |
| /* |
| * Userspace asked us to scrub something, it's broken, and we have no |
| * way of fixing it. Scream in the logs. |
| */ |
| if (sc->sm->sm_flags & (XFS_SCRUB_OFLAG_CORRUPT | |
| XFS_SCRUB_OFLAG_XCORRUPT)) |
| xfs_alert_ratelimited(sc->mp, |
| "Corruption detected during scrub."); |
| } |
| #endif /* CONFIG_XFS_ONLINE_REPAIR */ |
| |
| /* Dispatch metadata scrubbing. */ |
| int |
| xfs_scrub_metadata( |
| struct file *file, |
| struct xfs_scrub_metadata *sm) |
| { |
| struct xchk_stats_run run = { }; |
| struct xfs_scrub *sc; |
| struct xfs_mount *mp = XFS_I(file_inode(file))->i_mount; |
| u64 check_start; |
| int error = 0; |
| |
| BUILD_BUG_ON(sizeof(meta_scrub_ops) != |
| (sizeof(struct xchk_meta_ops) * XFS_SCRUB_TYPE_NR)); |
| |
| trace_xchk_start(XFS_I(file_inode(file)), sm, error); |
| |
| /* Forbidden if we are shut down or mounted norecovery. */ |
| error = -ESHUTDOWN; |
| if (xfs_is_shutdown(mp)) |
| goto out; |
| error = -ENOTRECOVERABLE; |
| if (xfs_has_norecovery(mp)) |
| goto out; |
| |
| error = xchk_validate_inputs(mp, sm); |
| if (error) |
| goto out; |
| |
| xfs_warn_mount(mp, XFS_OPSTATE_WARNED_SCRUB, |
| "EXPERIMENTAL online scrub feature in use. Use at your own risk!"); |
| |
| sc = kzalloc(sizeof(struct xfs_scrub), XCHK_GFP_FLAGS); |
| if (!sc) { |
| error = -ENOMEM; |
| goto out; |
| } |
| |
| sc->mp = mp; |
| sc->file = file; |
| sc->sm = sm; |
| sc->ops = &meta_scrub_ops[sm->sm_type]; |
| sc->sick_mask = xchk_health_mask_for_scrub_type(sm->sm_type); |
| retry_op: |
| /* |
| * When repairs are allowed, prevent freezing or readonly remount while |
| * scrub is running with a real transaction. |
| */ |
| if (sm->sm_flags & XFS_SCRUB_IFLAG_REPAIR) { |
| error = mnt_want_write_file(sc->file); |
| if (error) |
| goto out_sc; |
| |
| sc->flags |= XCHK_HAVE_FREEZE_PROT; |
| } |
| |
| /* Set up for the operation. */ |
| error = sc->ops->setup(sc); |
| if (error == -EDEADLOCK && !(sc->flags & XCHK_TRY_HARDER)) |
| goto try_harder; |
| if (error == -ECHRNG && !(sc->flags & XCHK_NEED_DRAIN)) |
| goto need_drain; |
| if (error) |
| goto out_teardown; |
| |
| /* Scrub for errors. */ |
| check_start = xchk_stats_now(); |
| error = sc->ops->scrub(sc); |
| run.scrub_ns += xchk_stats_elapsed_ns(check_start); |
| if (error == -EDEADLOCK && !(sc->flags & XCHK_TRY_HARDER)) |
| goto try_harder; |
| if (error == -ECHRNG && !(sc->flags & XCHK_NEED_DRAIN)) |
| goto need_drain; |
| if (error || (sm->sm_flags & XFS_SCRUB_OFLAG_INCOMPLETE)) |
| goto out_teardown; |
| |
| xchk_update_health(sc); |
| |
| if ((sc->sm->sm_flags & XFS_SCRUB_IFLAG_REPAIR) && |
| !(sc->flags & XREP_ALREADY_FIXED)) { |
| bool needs_fix = xchk_needs_repair(sc->sm); |
| |
| /* Userspace asked us to rebuild the structure regardless. */ |
| if (sc->sm->sm_flags & XFS_SCRUB_IFLAG_FORCE_REBUILD) |
| needs_fix = true; |
| |
| /* Let debug users force us into the repair routines. */ |
| if (XFS_TEST_ERROR(needs_fix, mp, XFS_ERRTAG_FORCE_SCRUB_REPAIR)) |
| needs_fix = true; |
| |
| /* |
| * If userspace asked for a repair but it wasn't necessary, |
| * report that back to userspace. |
| */ |
| if (!needs_fix) { |
| sc->sm->sm_flags |= XFS_SCRUB_OFLAG_NO_REPAIR_NEEDED; |
| goto out_nofix; |
| } |
| |
| /* |
| * If it's broken, userspace wants us to fix it, and we haven't |
| * already tried to fix it, then attempt a repair. |
| */ |
| error = xrep_attempt(sc, &run); |
| if (error == -EAGAIN) { |
| /* |
| * Either the repair function succeeded or it couldn't |
| * get all the resources it needs; either way, we go |
| * back to the beginning and call the scrub function. |
| */ |
| error = xchk_teardown(sc, 0); |
| if (error) { |
| xrep_failure(mp); |
| goto out_sc; |
| } |
| goto retry_op; |
| } |
| } |
| |
| out_nofix: |
| xchk_postmortem(sc); |
| out_teardown: |
| error = xchk_teardown(sc, error); |
| out_sc: |
| if (error != -ENOENT) |
| xchk_stats_merge(mp, sm, &run); |
| kfree(sc); |
| out: |
| trace_xchk_done(XFS_I(file_inode(file)), sm, error); |
| if (error == -EFSCORRUPTED || error == -EFSBADCRC) { |
| sm->sm_flags |= XFS_SCRUB_OFLAG_CORRUPT; |
| error = 0; |
| } |
| return error; |
| need_drain: |
| error = xchk_teardown(sc, 0); |
| if (error) |
| goto out_sc; |
| sc->flags |= XCHK_NEED_DRAIN; |
| run.retries++; |
| goto retry_op; |
| try_harder: |
| /* |
| * Scrubbers return -EDEADLOCK to mean 'try harder'. Tear down |
| * everything we hold, then set up again with preparation for |
| * worst-case scenarios. |
| */ |
| error = xchk_teardown(sc, 0); |
| if (error) |
| goto out_sc; |
| sc->flags |= XCHK_TRY_HARDER; |
| run.retries++; |
| goto retry_op; |
| } |