linux/fs/xfs/xfs_attr_list.c
<<
>>
Prefs
   1/*
   2 * Copyright (c) 2000-2005 Silicon Graphics, Inc.
   3 * Copyright (c) 2013 Red Hat, Inc.
   4 * All Rights Reserved.
   5 *
   6 * This program is free software; you can redistribute it and/or
   7 * modify it under the terms of the GNU General Public License as
   8 * published by the Free Software Foundation.
   9 *
  10 * This program is distributed in the hope that it would be useful,
  11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13 * GNU General Public License for more details.
  14 *
  15 * You should have received a copy of the GNU General Public License
  16 * along with this program; if not, write the Free Software Foundation,
  17 * Inc.,  51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  18 */
  19#include "xfs.h"
  20#include "xfs_fs.h"
  21#include "xfs_format.h"
  22#include "xfs_log_format.h"
  23#include "xfs_trans_resv.h"
  24#include "xfs_bit.h"
  25#include "xfs_mount.h"
  26#include "xfs_da_format.h"
  27#include "xfs_da_btree.h"
  28#include "xfs_inode.h"
  29#include "xfs_trans.h"
  30#include "xfs_inode_item.h"
  31#include "xfs_bmap.h"
  32#include "xfs_attr.h"
  33#include "xfs_attr_sf.h"
  34#include "xfs_attr_remote.h"
  35#include "xfs_attr_leaf.h"
  36#include "xfs_error.h"
  37#include "xfs_trace.h"
  38#include "xfs_buf_item.h"
  39#include "xfs_cksum.h"
  40#include "xfs_dir2.h"
  41
  42STATIC int
  43xfs_attr_shortform_compare(const void *a, const void *b)
  44{
  45        xfs_attr_sf_sort_t *sa, *sb;
  46
  47        sa = (xfs_attr_sf_sort_t *)a;
  48        sb = (xfs_attr_sf_sort_t *)b;
  49        if (sa->hash < sb->hash) {
  50                return -1;
  51        } else if (sa->hash > sb->hash) {
  52                return 1;
  53        } else {
  54                return sa->entno - sb->entno;
  55        }
  56}
  57
  58#define XFS_ISRESET_CURSOR(cursor) \
  59        (!((cursor)->initted) && !((cursor)->hashval) && \
  60         !((cursor)->blkno) && !((cursor)->offset))
  61/*
  62 * Copy out entries of shortform attribute lists for attr_list().
  63 * Shortform attribute lists are not stored in hashval sorted order.
  64 * If the output buffer is not large enough to hold them all, then we
  65 * we have to calculate each entries' hashvalue and sort them before
  66 * we can begin returning them to the user.
  67 */
  68int
  69xfs_attr_shortform_list(xfs_attr_list_context_t *context)
  70{
  71        attrlist_cursor_kern_t *cursor;
  72        xfs_attr_sf_sort_t *sbuf, *sbp;
  73        xfs_attr_shortform_t *sf;
  74        xfs_attr_sf_entry_t *sfe;
  75        xfs_inode_t *dp;
  76        int sbsize, nsbuf, count, i;
  77        int error;
  78
  79        ASSERT(context != NULL);
  80        dp = context->dp;
  81        ASSERT(dp != NULL);
  82        ASSERT(dp->i_afp != NULL);
  83        sf = (xfs_attr_shortform_t *)dp->i_afp->if_u1.if_data;
  84        ASSERT(sf != NULL);
  85        if (!sf->hdr.count)
  86                return 0;
  87        cursor = context->cursor;
  88        ASSERT(cursor != NULL);
  89
  90        trace_xfs_attr_list_sf(context);
  91
  92        /*
  93         * If the buffer is large enough and the cursor is at the start,
  94         * do not bother with sorting since we will return everything in
  95         * one buffer and another call using the cursor won't need to be
  96         * made.
  97         * Note the generous fudge factor of 16 overhead bytes per entry.
  98         * If bufsize is zero then put_listent must be a search function
  99         * and can just scan through what we have.
 100         */
 101        if (context->bufsize == 0 ||
 102            (XFS_ISRESET_CURSOR(cursor) &&
 103             (dp->i_afp->if_bytes + sf->hdr.count * 16) < context->bufsize)) {
 104                for (i = 0, sfe = &sf->list[0]; i < sf->hdr.count; i++) {
 105                        error = context->put_listent(context,
 106                                           sfe->flags,
 107                                           sfe->nameval,
 108                                           (int)sfe->namelen,
 109                                           (int)sfe->valuelen,
 110                                           &sfe->nameval[sfe->namelen]);
 111
 112                        /*
 113                         * Either search callback finished early or
 114                         * didn't fit it all in the buffer after all.
 115                         */
 116                        if (context->seen_enough)
 117                                break;
 118
 119                        if (error)
 120                                return error;
 121                        sfe = XFS_ATTR_SF_NEXTENTRY(sfe);
 122                }
 123                trace_xfs_attr_list_sf_all(context);
 124                return 0;
 125        }
 126
 127        /* do no more for a search callback */
 128        if (context->bufsize == 0)
 129                return 0;
 130
 131        /*
 132         * It didn't all fit, so we have to sort everything on hashval.
 133         */
 134        sbsize = sf->hdr.count * sizeof(*sbuf);
 135        sbp = sbuf = kmem_alloc(sbsize, KM_SLEEP | KM_NOFS);
 136
 137        /*
 138         * Scan the attribute list for the rest of the entries, storing
 139         * the relevant info from only those that match into a buffer.
 140         */
 141        nsbuf = 0;
 142        for (i = 0, sfe = &sf->list[0]; i < sf->hdr.count; i++) {
 143                if (unlikely(
 144                    ((char *)sfe < (char *)sf) ||
 145                    ((char *)sfe >= ((char *)sf + dp->i_afp->if_bytes)))) {
 146                        XFS_CORRUPTION_ERROR("xfs_attr_shortform_list",
 147                                             XFS_ERRLEVEL_LOW,
 148                                             context->dp->i_mount, sfe);
 149                        kmem_free(sbuf);
 150                        return -EFSCORRUPTED;
 151                }
 152
 153                sbp->entno = i;
 154                sbp->hash = xfs_da_hashname(sfe->nameval, sfe->namelen);
 155                sbp->name = sfe->nameval;
 156                sbp->namelen = sfe->namelen;
 157                /* These are bytes, and both on-disk, don't endian-flip */
 158                sbp->valuelen = sfe->valuelen;
 159                sbp->flags = sfe->flags;
 160                sfe = XFS_ATTR_SF_NEXTENTRY(sfe);
 161                sbp++;
 162                nsbuf++;
 163        }
 164
 165        /*
 166         * Sort the entries on hash then entno.
 167         */
 168        xfs_sort(sbuf, nsbuf, sizeof(*sbuf), xfs_attr_shortform_compare);
 169
 170        /*
 171         * Re-find our place IN THE SORTED LIST.
 172         */
 173        count = 0;
 174        cursor->initted = 1;
 175        cursor->blkno = 0;
 176        for (sbp = sbuf, i = 0; i < nsbuf; i++, sbp++) {
 177                if (sbp->hash == cursor->hashval) {
 178                        if (cursor->offset == count) {
 179                                break;
 180                        }
 181                        count++;
 182                } else if (sbp->hash > cursor->hashval) {
 183                        break;
 184                }
 185        }
 186        if (i == nsbuf) {
 187                kmem_free(sbuf);
 188                return 0;
 189        }
 190
 191        /*
 192         * Loop putting entries into the user buffer.
 193         */
 194        for ( ; i < nsbuf; i++, sbp++) {
 195                if (cursor->hashval != sbp->hash) {
 196                        cursor->hashval = sbp->hash;
 197                        cursor->offset = 0;
 198                }
 199                error = context->put_listent(context,
 200                                        sbp->flags,
 201                                        sbp->name,
 202                                        sbp->namelen,
 203                                        sbp->valuelen,
 204                                        &sbp->name[sbp->namelen]);
 205                if (error) {
 206                        kmem_free(sbuf);
 207                        return error;
 208                }
 209                if (context->seen_enough)
 210                        break;
 211                cursor->offset++;
 212        }
 213
 214        kmem_free(sbuf);
 215        return 0;
 216}
 217
 218STATIC int
 219xfs_attr_node_list(xfs_attr_list_context_t *context)
 220{
 221        attrlist_cursor_kern_t *cursor;
 222        xfs_attr_leafblock_t *leaf;
 223        xfs_da_intnode_t *node;
 224        struct xfs_attr3_icleaf_hdr leafhdr;
 225        struct xfs_da3_icnode_hdr nodehdr;
 226        struct xfs_da_node_entry *btree;
 227        int error, i;
 228        struct xfs_buf *bp;
 229        struct xfs_inode        *dp = context->dp;
 230        struct xfs_mount        *mp = dp->i_mount;
 231
 232        trace_xfs_attr_node_list(context);
 233
 234        cursor = context->cursor;
 235        cursor->initted = 1;
 236
 237        /*
 238         * Do all sorts of validation on the passed-in cursor structure.
 239         * If anything is amiss, ignore the cursor and look up the hashval
 240         * starting from the btree root.
 241         */
 242        bp = NULL;
 243        if (cursor->blkno > 0) {
 244                error = xfs_da3_node_read(NULL, dp, cursor->blkno, -1,
 245                                              &bp, XFS_ATTR_FORK);
 246                if ((error != 0) && (error != -EFSCORRUPTED))
 247                        return error;
 248                if (bp) {
 249                        struct xfs_attr_leaf_entry *entries;
 250
 251                        node = bp->b_addr;
 252                        switch (be16_to_cpu(node->hdr.info.magic)) {
 253                        case XFS_DA_NODE_MAGIC:
 254                        case XFS_DA3_NODE_MAGIC:
 255                                trace_xfs_attr_list_wrong_blk(context);
 256                                xfs_trans_brelse(NULL, bp);
 257                                bp = NULL;
 258                                break;
 259                        case XFS_ATTR_LEAF_MAGIC:
 260                        case XFS_ATTR3_LEAF_MAGIC:
 261                                leaf = bp->b_addr;
 262                                xfs_attr3_leaf_hdr_from_disk(mp->m_attr_geo,
 263                                                             &leafhdr, leaf);
 264                                entries = xfs_attr3_leaf_entryp(leaf);
 265                                if (cursor->hashval > be32_to_cpu(
 266                                                entries[leafhdr.count - 1].hashval)) {
 267                                        trace_xfs_attr_list_wrong_blk(context);
 268                                        xfs_trans_brelse(NULL, bp);
 269                                        bp = NULL;
 270                                } else if (cursor->hashval <= be32_to_cpu(
 271                                                entries[0].hashval)) {
 272                                        trace_xfs_attr_list_wrong_blk(context);
 273                                        xfs_trans_brelse(NULL, bp);
 274                                        bp = NULL;
 275                                }
 276                                break;
 277                        default:
 278                                trace_xfs_attr_list_wrong_blk(context);
 279                                xfs_trans_brelse(NULL, bp);
 280                                bp = NULL;
 281                        }
 282                }
 283        }
 284
 285        /*
 286         * We did not find what we expected given the cursor's contents,
 287         * so we start from the top and work down based on the hash value.
 288         * Note that start of node block is same as start of leaf block.
 289         */
 290        if (bp == NULL) {
 291                cursor->blkno = 0;
 292                for (;;) {
 293                        __uint16_t magic;
 294
 295                        error = xfs_da3_node_read(NULL, dp,
 296                                                      cursor->blkno, -1, &bp,
 297                                                      XFS_ATTR_FORK);
 298                        if (error)
 299                                return error;
 300                        node = bp->b_addr;
 301                        magic = be16_to_cpu(node->hdr.info.magic);
 302                        if (magic == XFS_ATTR_LEAF_MAGIC ||
 303                            magic == XFS_ATTR3_LEAF_MAGIC)
 304                                break;
 305                        if (magic != XFS_DA_NODE_MAGIC &&
 306                            magic != XFS_DA3_NODE_MAGIC) {
 307                                XFS_CORRUPTION_ERROR("xfs_attr_node_list(3)",
 308                                                     XFS_ERRLEVEL_LOW,
 309                                                     context->dp->i_mount,
 310                                                     node);
 311                                xfs_trans_brelse(NULL, bp);
 312                                return -EFSCORRUPTED;
 313                        }
 314
 315                        dp->d_ops->node_hdr_from_disk(&nodehdr, node);
 316                        btree = dp->d_ops->node_tree_p(node);
 317                        for (i = 0; i < nodehdr.count; btree++, i++) {
 318                                if (cursor->hashval
 319                                                <= be32_to_cpu(btree->hashval)) {
 320                                        cursor->blkno = be32_to_cpu(btree->before);
 321                                        trace_xfs_attr_list_node_descend(context,
 322                                                                         btree);
 323                                        break;
 324                                }
 325                        }
 326                        if (i == nodehdr.count) {
 327                                xfs_trans_brelse(NULL, bp);
 328                                return 0;
 329                        }
 330                        xfs_trans_brelse(NULL, bp);
 331                }
 332        }
 333        ASSERT(bp != NULL);
 334
 335        /*
 336         * Roll upward through the blocks, processing each leaf block in
 337         * order.  As long as there is space in the result buffer, keep
 338         * adding the information.
 339         */
 340        for (;;) {
 341                leaf = bp->b_addr;
 342                error = xfs_attr3_leaf_list_int(bp, context);
 343                if (error) {
 344                        xfs_trans_brelse(NULL, bp);
 345                        return error;
 346                }
 347                xfs_attr3_leaf_hdr_from_disk(mp->m_attr_geo, &leafhdr, leaf);
 348                if (context->seen_enough || leafhdr.forw == 0)
 349                        break;
 350                cursor->blkno = leafhdr.forw;
 351                xfs_trans_brelse(NULL, bp);
 352                error = xfs_attr3_leaf_read(NULL, dp, cursor->blkno, -1, &bp);
 353                if (error)
 354                        return error;
 355        }
 356        xfs_trans_brelse(NULL, bp);
 357        return 0;
 358}
 359
 360/*
 361 * Copy out attribute list entries for attr_list(), for leaf attribute lists.
 362 */
 363int
 364xfs_attr3_leaf_list_int(
 365        struct xfs_buf                  *bp,
 366        struct xfs_attr_list_context    *context)
 367{
 368        struct attrlist_cursor_kern     *cursor;
 369        struct xfs_attr_leafblock       *leaf;
 370        struct xfs_attr3_icleaf_hdr     ichdr;
 371        struct xfs_attr_leaf_entry      *entries;
 372        struct xfs_attr_leaf_entry      *entry;
 373        int                             retval;
 374        int                             i;
 375        struct xfs_mount                *mp = context->dp->i_mount;
 376
 377        trace_xfs_attr_list_leaf(context);
 378
 379        leaf = bp->b_addr;
 380        xfs_attr3_leaf_hdr_from_disk(mp->m_attr_geo, &ichdr, leaf);
 381        entries = xfs_attr3_leaf_entryp(leaf);
 382
 383        cursor = context->cursor;
 384        cursor->initted = 1;
 385
 386        /*
 387         * Re-find our place in the leaf block if this is a new syscall.
 388         */
 389        if (context->resynch) {
 390                entry = &entries[0];
 391                for (i = 0; i < ichdr.count; entry++, i++) {
 392                        if (be32_to_cpu(entry->hashval) == cursor->hashval) {
 393                                if (cursor->offset == context->dupcnt) {
 394                                        context->dupcnt = 0;
 395                                        break;
 396                                }
 397                                context->dupcnt++;
 398                        } else if (be32_to_cpu(entry->hashval) >
 399                                        cursor->hashval) {
 400                                context->dupcnt = 0;
 401                                break;
 402                        }
 403                }
 404                if (i == ichdr.count) {
 405                        trace_xfs_attr_list_notfound(context);
 406                        return 0;
 407                }
 408        } else {
 409                entry = &entries[0];
 410                i = 0;
 411        }
 412        context->resynch = 0;
 413
 414        /*
 415         * We have found our place, start copying out the new attributes.
 416         */
 417        retval = 0;
 418        for (; i < ichdr.count; entry++, i++) {
 419                if (be32_to_cpu(entry->hashval) != cursor->hashval) {
 420                        cursor->hashval = be32_to_cpu(entry->hashval);
 421                        cursor->offset = 0;
 422                }
 423
 424                if (entry->flags & XFS_ATTR_INCOMPLETE)
 425                        continue;               /* skip incomplete entries */
 426
 427                if (entry->flags & XFS_ATTR_LOCAL) {
 428                        xfs_attr_leaf_name_local_t *name_loc =
 429                                xfs_attr3_leaf_name_local(leaf, i);
 430
 431                        retval = context->put_listent(context,
 432                                                entry->flags,
 433                                                name_loc->nameval,
 434                                                (int)name_loc->namelen,
 435                                                be16_to_cpu(name_loc->valuelen),
 436                                                &name_loc->nameval[name_loc->namelen]);
 437                        if (retval)
 438                                return retval;
 439                } else {
 440                        xfs_attr_leaf_name_remote_t *name_rmt =
 441                                xfs_attr3_leaf_name_remote(leaf, i);
 442
 443                        int valuelen = be32_to_cpu(name_rmt->valuelen);
 444
 445                        if (context->put_value) {
 446                                xfs_da_args_t args;
 447
 448                                memset((char *)&args, 0, sizeof(args));
 449                                args.geo = context->dp->i_mount->m_attr_geo;
 450                                args.dp = context->dp;
 451                                args.whichfork = XFS_ATTR_FORK;
 452                                args.valuelen = valuelen;
 453                                args.rmtvaluelen = valuelen;
 454                                args.value = kmem_alloc(valuelen, KM_SLEEP | KM_NOFS);
 455                                args.rmtblkno = be32_to_cpu(name_rmt->valueblk);
 456                                args.rmtblkcnt = xfs_attr3_rmt_blocks(
 457                                                        args.dp->i_mount, valuelen);
 458                                retval = xfs_attr_rmtval_get(&args);
 459                                if (!retval)
 460                                        retval = context->put_listent(context,
 461                                                        entry->flags,
 462                                                        name_rmt->name,
 463                                                        (int)name_rmt->namelen,
 464                                                        valuelen,
 465                                                        args.value);
 466                                kmem_free(args.value);
 467                        } else {
 468                                retval = context->put_listent(context,
 469                                                entry->flags,
 470                                                name_rmt->name,
 471                                                (int)name_rmt->namelen,
 472                                                valuelen,
 473                                                NULL);
 474                        }
 475                        if (retval)
 476                                return retval;
 477                }
 478                if (context->seen_enough)
 479                        break;
 480                cursor->offset++;
 481        }
 482        trace_xfs_attr_list_leaf_end(context);
 483        return retval;
 484}
 485
 486/*
 487 * Copy out attribute entries for attr_list(), for leaf attribute lists.
 488 */
 489STATIC int
 490xfs_attr_leaf_list(xfs_attr_list_context_t *context)
 491{
 492        int error;
 493        struct xfs_buf *bp;
 494
 495        trace_xfs_attr_leaf_list(context);
 496
 497        context->cursor->blkno = 0;
 498        error = xfs_attr3_leaf_read(NULL, context->dp, 0, -1, &bp);
 499        if (error)
 500                return error;
 501
 502        error = xfs_attr3_leaf_list_int(bp, context);
 503        xfs_trans_brelse(NULL, bp);
 504        return error;
 505}
 506
 507int
 508xfs_attr_list_int(
 509        xfs_attr_list_context_t *context)
 510{
 511        int error;
 512        xfs_inode_t *dp = context->dp;
 513        uint            lock_mode;
 514
 515        XFS_STATS_INC(dp->i_mount, xs_attr_list);
 516
 517        if (XFS_FORCED_SHUTDOWN(dp->i_mount))
 518                return -EIO;
 519
 520        /*
 521         * Decide on what work routines to call based on the inode size.
 522         */
 523        lock_mode = xfs_ilock_attr_map_shared(dp);
 524        if (!xfs_inode_hasattr(dp)) {
 525                error = 0;
 526        } else if (dp->i_d.di_aformat == XFS_DINODE_FMT_LOCAL) {
 527                error = xfs_attr_shortform_list(context);
 528        } else if (xfs_bmap_one_block(dp, XFS_ATTR_FORK)) {
 529                error = xfs_attr_leaf_list(context);
 530        } else {
 531                error = xfs_attr_node_list(context);
 532        }
 533        xfs_iunlock(dp, lock_mode);
 534        return error;
 535}
 536
 537#define ATTR_ENTBASESIZE                /* minimum bytes used by an attr */ \
 538        (((struct attrlist_ent *) 0)->a_name - (char *) 0)
 539#define ATTR_ENTSIZE(namelen)           /* actual bytes used by an attr */ \
 540        ((ATTR_ENTBASESIZE + (namelen) + 1 + sizeof(u_int32_t)-1) \
 541         & ~(sizeof(u_int32_t)-1))
 542
 543/*
 544 * Format an attribute and copy it out to the user's buffer.
 545 * Take care to check values and protect against them changing later,
 546 * we may be reading them directly out of a user buffer.
 547 */
 548STATIC int
 549xfs_attr_put_listent(
 550        xfs_attr_list_context_t *context,
 551        int             flags,
 552        unsigned char   *name,
 553        int             namelen,
 554        int             valuelen,
 555        unsigned char   *value)
 556{
 557        struct attrlist *alist = (struct attrlist *)context->alist;
 558        attrlist_ent_t *aep;
 559        int arraytop;
 560
 561        ASSERT(!(context->flags & ATTR_KERNOVAL));
 562        ASSERT(context->count >= 0);
 563        ASSERT(context->count < (ATTR_MAX_VALUELEN/8));
 564        ASSERT(context->firstu >= sizeof(*alist));
 565        ASSERT(context->firstu <= context->bufsize);
 566
 567        /*
 568         * Only list entries in the right namespace.
 569         */
 570        if (((context->flags & ATTR_SECURE) == 0) !=
 571            ((flags & XFS_ATTR_SECURE) == 0))
 572                return 0;
 573        if (((context->flags & ATTR_ROOT) == 0) !=
 574            ((flags & XFS_ATTR_ROOT) == 0))
 575                return 0;
 576
 577        arraytop = sizeof(*alist) +
 578                        context->count * sizeof(alist->al_offset[0]);
 579        context->firstu -= ATTR_ENTSIZE(namelen);
 580        if (context->firstu < arraytop) {
 581                trace_xfs_attr_list_full(context);
 582                alist->al_more = 1;
 583                context->seen_enough = 1;
 584                return 1;
 585        }
 586
 587        aep = (attrlist_ent_t *)&context->alist[context->firstu];
 588        aep->a_valuelen = valuelen;
 589        memcpy(aep->a_name, name, namelen);
 590        aep->a_name[namelen] = 0;
 591        alist->al_offset[context->count++] = context->firstu;
 592        alist->al_count = context->count;
 593        trace_xfs_attr_list_add(context);
 594        return 0;
 595}
 596
 597/*
 598 * Generate a list of extended attribute names and optionally
 599 * also value lengths.  Positive return value follows the XFS
 600 * convention of being an error, zero or negative return code
 601 * is the length of the buffer returned (negated), indicating
 602 * success.
 603 */
 604int
 605xfs_attr_list(
 606        xfs_inode_t     *dp,
 607        char            *buffer,
 608        int             bufsize,
 609        int             flags,
 610        attrlist_cursor_kern_t *cursor)
 611{
 612        xfs_attr_list_context_t context;
 613        struct attrlist *alist;
 614        int error;
 615
 616        /*
 617         * Validate the cursor.
 618         */
 619        if (cursor->pad1 || cursor->pad2)
 620                return -EINVAL;
 621        if ((cursor->initted == 0) &&
 622            (cursor->hashval || cursor->blkno || cursor->offset))
 623                return -EINVAL;
 624
 625        /*
 626         * Check for a properly aligned buffer.
 627         */
 628        if (((long)buffer) & (sizeof(int)-1))
 629                return -EFAULT;
 630        if (flags & ATTR_KERNOVAL)
 631                bufsize = 0;
 632
 633        /*
 634         * Initialize the output buffer.
 635         */
 636        memset(&context, 0, sizeof(context));
 637        context.dp = dp;
 638        context.cursor = cursor;
 639        context.resynch = 1;
 640        context.flags = flags;
 641        context.alist = buffer;
 642        context.bufsize = (bufsize & ~(sizeof(int)-1));  /* align */
 643        context.firstu = context.bufsize;
 644        context.put_listent = xfs_attr_put_listent;
 645
 646        alist = (struct attrlist *)context.alist;
 647        alist->al_count = 0;
 648        alist->al_more = 0;
 649        alist->al_offset[0] = context.bufsize;
 650
 651        error = xfs_attr_list_int(&context);
 652        ASSERT(error <= 0);
 653        return error;
 654}
 655