- diff --git a/cygwin/devel/msnfs41client.bash b/cygwin/devel/msnfs41client.bash
 - index 4a391dd..8c7bfa4 100755
 - --- a/cygwin/devel/msnfs41client.bash
 - +++ b/cygwin/devel/msnfs41client.bash
 - @@ -467,7 +467,7 @@ function nfsclient_rundeamon
 - "${nfsd_args[@]}"
 - )
 - "${nfsd_args[@]}"
 - - elif false ; then
 - + elif true ; then
 - export _NT_ALT_SYMBOL_PATH="$(cygpath -w "$PWD");srv*https://msdl.microsoft.com/download/symbols"
 - #
 - # Useful cdb cmds:
 - @@ -601,7 +601,7 @@ function nfsclient_system_rundeamon
 - )
 - "${nfsd_args[@]}"
 - - elif false ; then
 - + elif true ; then
 - export _NT_ALT_SYMBOL_PATH="$(cygpath -w "$PWD");srv*https://msdl.microsoft.com/download/symbols"
 - # - heap tests (eats lots of memory):
 - # '-c' '!gflag +full;g' for heap tests
 - diff --git a/daemon/acl.c b/daemon/acl.c
 - index 370f2d3..c655b3f 100644
 - --- a/daemon/acl.c
 - +++ b/daemon/acl.c
 - @@ -326,62 +326,49 @@ static int handle_getacl(void *daemon_context, nfs41_upcall *upcall)
 - PSID osid = NULL, gsid = NULL;
 - DWORD sid_len;
 - char owner[NFS4_FATTR4_OWNER_LIMIT+1], group[NFS4_FATTR4_OWNER_LIMIT+1];
 - + bitmap4 owner_group_acl_bitmap = {
 - + .count = 2,
 - + .arr[0] = 0,
 - + .arr[1] = FATTR4_WORD1_OWNER|FATTR4_WORD1_OWNER_GROUP
 - + };
 - nfsacl41 acl = { 0 };
 - DPRINTF(ACLLVL1, ("--> handle_getacl(state->path.path='%s')\n",
 - state->path.path));
 - if (args->query & DACL_SECURITY_INFORMATION) {
 - -use_nfs41_getattr:
 - - bitmap4 attr_request = { 0 };
 - - (void)memset(&info, 0, sizeof(nfs41_file_info));
 - - info.owner = owner;
 - - info.owner_group = group;
 - -
 - - attr_request.count = 2;
 - - attr_request.arr[0] = FATTR4_WORD0_ACL;
 - - attr_request.arr[1] = FATTR4_WORD1_OWNER | FATTR4_WORD1_OWNER_GROUP;
 - - info.acl = &acl;
 - - status = nfs41_getattr(state->session, &state->file, &attr_request, &info);
 - - if (status) {
 - - eprintf("handle_getacl: nfs41_getattr() failed with %d\n",
 - - status);
 - - goto out;
 - - }
 - + owner_group_acl_bitmap.arr[0] |= FATTR4_WORD0_ACL;
 - }
 - - else {
 - - (void)memset(&info, 0, sizeof(nfs41_file_info));
 - - info.owner = owner;
 - - info.owner_group = group;
 - - status = nfs41_cached_getattr(state->session, &state->file, &info);
 - - if (status) {
 - - eprintf("handle_getacl: nfs41_cached_getattr() failed with %d\n",
 - - status);
 - - goto out;
 - - }
 - + (void)memset(&info, 0, sizeof(nfs41_file_info));
 - + info.owner = owner;
 - + info.owner_group = group;
 - + info.acl = &acl;
 - - EASSERT(info.attrmask.count > 1);
 - -
 - - /*
 - - * In rare cases owner/owner_group are not in the cache
 - - * (usually for new files). In this case do a full
 - - * roundtrip to the NFS server to get the data...
 - - */
 - - if ((bitmap_isset(&info.attrmask, 1,
 - - FATTR4_WORD1_OWNER) == false) ||
 - - (bitmap_isset(&info.attrmask, 1,
 - - FATTR4_WORD1_OWNER_GROUP) == false)) {
 - - DPRINTF(ACLLVL2, ("handle_getattr: owner/owner_group not in cache, doing full lookup...\n"));
 - - goto use_nfs41_getattr;
 - - }
 - + /*
 - + * |nfs41_cached_getattr()| will first try to get all information from
 - + * the cache. But if bits are missing (e.g. |FATTR4_WORD0_ACL|, because
 - + * either not cached in the name cache, or name cache entry for ACLs
 - + * expired...), then this will do a server roundtrip to get the missing
 - + * data
 - + */
 - + status = nfs41_cached_getattr(state->session,
 - + &state->file, &owner_group_acl_bitmap, &info);
 - + if (status) {
 - + eprintf("handle_getacl: nfs41_cached_getattr() failed with %d\n",
 - + status);
 - + goto out;
 - }
 - EASSERT(info.attrmask.count > 1);
 - - EASSERT(bitmap_isset(&info.attrmask, 1, FATTR4_WORD1_OWNER) &&
 - - bitmap_isset(&info.attrmask, 1, FATTR4_WORD1_OWNER_GROUP));
 - if (args->query & DACL_SECURITY_INFORMATION) {
 - - EASSERT(bitmap_isset(&info.attrmask, 0, FATTR4_WORD0_ACL));
 - + EASSERT(bitmap_isset(&info.attrmask, 0, FATTR4_WORD0_ACL) == true);
 - + }
 - + if (args->query & OWNER_SECURITY_INFORMATION) {
 - + EASSERT(bitmap_isset(&info.attrmask, 1, FATTR4_WORD1_OWNER) == true);
 - + }
 - + if (args->query & GROUP_SECURITY_INFORMATION) {
 - + EASSERT(bitmap_isset(&info.attrmask, 1, FATTR4_WORD1_OWNER_GROUP) == true);
 - }
 - status = InitializeSecurityDescriptor(&sec_desc,
 - @@ -433,6 +420,7 @@ use_nfs41_getattr:
 - goto out;
 - }
 - }
 - +
 - if (args->query & DACL_SECURITY_INFORMATION) {
 - DPRINTF(ACLLVL2, ("handle_getacl: DACL_SECURITY_INFORMATION\n"));
 - status = convert_nfs4acl_2_dacl(nfs41dg,
 - diff --git a/daemon/getattr.c b/daemon/getattr.c
 - index 7c8b2d1..fb7a738 100644
 - --- a/daemon/getattr.c
 - +++ b/daemon/getattr.c
 - @@ -37,18 +37,39 @@
 - int nfs41_cached_getattr(
 - IN nfs41_session *session,
 - IN nfs41_path_fh *file,
 - + IN OPTIONAL bitmap4 *extra_attr_request,
 - OUT nfs41_file_info *info)
 - {
 - int status;
 - + bool bits_missing = false;
 - /* first look for cached attributes */
 - status = nfs41_attr_cache_lookup(session_name_cache(session),
 - file->fh.fileid, info);
 - -
 - - if (status) {
 - +
 - + if ((status == 0) && extra_attr_request) {
 - + uint32_t i;
 - +
 - + /* Check if bits are missing... */
 - + for (i=0 ; i < extra_attr_request->count ; i++) {
 - + if ((extra_attr_request->arr[i] != 0) &&
 - + ((((i < info->attrmask.count)?(info->attrmask.arr[i]):0) &
 - + extra_attr_request->arr[i]) == 0)) {
 - + bits_missing = true;
 - + DPRINTF(1, ("nfs41_cached_getattr: bits missing %d\n", i));
 - + break;
 - + }
 - + }
 - + }
 - +
 - + if (status || bits_missing) {
 - /* fetch attributes from the server */
 - bitmap4 attr_request;
 - - nfs41_superblock_getattr_mask(file->fh.superblock, &attr_request);
 - + nfs41_superblock_getattr_mask(file->fh.superblock, &attr_request);
 - +
 - + if (extra_attr_request) {
 - + bitmap_or(&attr_request, extra_attr_request);
 - + }
 - status = nfs41_getattr(session, file, &attr_request, info);
 - if (status) {
 - @@ -88,7 +109,7 @@ static int handle_getattr(void *daemon_context, nfs41_upcall *upcall)
 - nfs41_open_state *state = upcall->state_ref;
 - nfs41_file_info info = { 0 };
 - - status = nfs41_cached_getattr(state->session, &state->file, &info);
 - + status = nfs41_cached_getattr(state->session, &state->file, NULL, &info);
 - if (status) {
 - eprintf("handle_getattr(state->path.path='%s'): "
 - "nfs41_cached_getattr() failed with %d\n",
 - @@ -98,7 +119,7 @@ static int handle_getattr(void *daemon_context, nfs41_upcall *upcall)
 - }
 - if (info.type == NF4LNK) {
 - - nfs41_file_info target_info;
 - + nfs41_file_info target_info = { 0 };
 - int target_status = nfs41_symlink_follow(upcall->root_ref,
 - state->session, &state->file, &target_info);
 - if (target_status == NO_ERROR) {
 - diff --git a/daemon/name_cache.c b/daemon/name_cache.c
 - index 950c188..2c9d90d 100644
 - --- a/daemon/name_cache.c
 - +++ b/daemon/name_cache.c
 - @@ -31,6 +31,7 @@
 - #include "util.h"
 - #include "tree.h"
 - #include "daemon_debug.h"
 - +#include "nfs41_driver.h" /* for |NFS41_ACL_CACHE_EXPIRATION| */
 - /* dprintf levels for name cache logging */
 - @@ -41,6 +42,7 @@ enum {
 - #define NAME_CACHE_EXPIRATION 30 /* TODO: get from configuration */
 - +#define NAME_CACHE_ACL_EXPIRATION NFS41_ACL_CACHE_EXPIRATION
 - /* allow up to 32M of memory for name and attribute cache entries */
 - #define NAME_CACHE_MAX_SIZE (32*1024*1024L)
 - @@ -87,11 +89,12 @@ static __inline bool_t is_delegation(
 - #define NC_ATTR_NUMLINKS (1 << 8)
 - #define NC_ATTR_OWNER (1 << 9)
 - #define NC_ATTR_OWNER_GROUP (1 << 10)
 - -#define NC_ATTR_TIME_ACCESS (1 << 11)
 - -#define NC_ATTR_TIME_CREATE (1 << 12)
 - -#define NC_ATTR_TIME_MODIFY (1 << 13)
 - -#define NC_ATTR_SYSTEM (1 << 14)
 - -#define NC_ATTR_CLONE_BLKSIZE (1 << 15)
 - +#define NC_ATTR_ACL (1 << 11)
 - +#define NC_ATTR_TIME_ACCESS (1 << 12)
 - +#define NC_ATTR_TIME_CREATE (1 << 13)
 - +#define NC_ATTR_TIME_MODIFY (1 << 14)
 - +#define NC_ATTR_SYSTEM (1 << 15)
 - +#define NC_ATTR_CLONE_BLKSIZE (1 << 16)
 - /* attribute cache */
 - struct attr_cache_entry {
 - @@ -123,6 +126,8 @@ struct attr_cache_entry {
 - unsigned delegated : 1;
 - char owner[NFS4_FATTR4_OWNER_LIMIT+1];
 - char owner_group[NFS4_FATTR4_OWNER_LIMIT+1];
 - + util_reltimestamp acl_expiration;
 - + nfsacl41 acl;
 - };
 - #define ATTR_ENTRY_SIZE sizeof(struct attr_cache_entry)
 - @@ -169,6 +174,7 @@ static int attr_cache_entry_create(
 - entry->fsid_minor = 0ULL;
 - entry->invalidated = FALSE;
 - entry->delegated = FALSE;
 - + (void)memset(&entry->acl, 0, sizeof(nfsacl41));
 - *entry_out = entry;
 - out:
 - return status;
 - @@ -180,6 +186,12 @@ static __inline void attr_cache_entry_free(
 - {
 - DPRINTF(NCLVL1, ("attr_cache_entry_free(%llu)\n", entry->fileid));
 - RB_REMOVE(attr_tree, &cache->head, entry);
 - +
 - + if (entry->acl.aces) {
 - + free(entry->acl.aces);
 - + entry->acl.aces = NULL;
 - + }
 - +
 - /* add it back to free_entries */
 - list_add_tail(&cache->free_entries, &entry->free_entry);
 - }
 - @@ -344,6 +356,22 @@ static void attr_cache_update(
 - entry->nc_attrs |= NC_ATTR_ARCHIVE;
 - entry->archive = info->archive;
 - }
 - + if (info->attrmask.arr[0] & FATTR4_WORD0_ACL) {
 - + if (entry->acl.aces) {
 - + free(entry->acl.aces);
 - + entry->acl.aces = NULL;
 - + }
 - +
 - + entry->nc_attrs &= ~NC_ATTR_ACL;
 - +
 - + if (info->acl) {
 - + entry->acl_expiration = UTIL_GETRELTIME() +
 - + NAME_CACHE_ACL_EXPIRATION;
 - + entry->nc_attrs |= NC_ATTR_ACL;
 - + (void)dup_nfsacl41aces(&entry->acl, info->acl);
 - + EASSERT(entry->acl.aces != NULL);
 - + }
 - + }
 - }
 - if (info->attrmask.count > 1) {
 - if (info->attrmask.arr[1] & FATTR4_WORD1_MODE) {
 - @@ -463,6 +491,22 @@ static void copy_attrs(
 - /* this should only happen for newly created files/dirs */
 - dst->owner_group = NULL;
 - }
 - + if (src->nc_attrs & NC_ATTR_ACL) {
 - + /*
 - + * 1. Only copy attribute if |dst| provides a pointer to an ACL struct
 - + * This implicitly solves the issue who will free |dst->acl.aces|,
 - + * because only who provides a |dst->acl| will get ACL/ACE data,
 - + * but then also has to free the |dst->acl.aces| data.
 - + * 2. Only copy data and |FATTR4_WORD0_ACL| if |acl_expiration| did
 - + * not indicate expiration. This has a subtle interaction with
 - + * |handle_getacl()|, where |nfs41_cached_getattr()| will fall-back
 - + * to do a server lookup if the |FATTR4_WORD0_ACL| bit is missing.
 - + */
 - + if (dst->acl && (UTIL_GETRELTIME() <= src->acl_expiration)) {
 - + dst->attrmask.arr[0] |= FATTR4_WORD0_ACL;
 - + (void)dup_nfsacl41aces(dst->acl, &src->acl);
 - + }
 - + }
 - if (src->nc_attrs & NC_ATTR_FSID) {
 - dst->attrmask.arr[0] |= FATTR4_WORD0_FSID;
 - dst->fsid.major = src->fsid_major;
 - diff --git a/daemon/nfs41_ops.c b/daemon/nfs41_ops.c
 - index 862ce0e..b5cda90 100644
 - --- a/daemon/nfs41_ops.c
 - +++ b/daemon/nfs41_ops.c
 - @@ -1478,28 +1478,30 @@ int nfs41_setattr(
 - nfs41_superblock_getattr_mask(file->fh.superblock, &attr_request);
 - - if (info->attrmask.count >= 2) {
 - - /*
 - - * If we set owner and/or owner_group make sure we ask
 - - * the server to send the values back for two reasons:
 - - * 1, Handle cases like "all_squash" (e.g. nfsd turns
 - - * owner/owner_group always into "nobody"/"nogroup")
 - - * 2. Make sure we update the name cache with the new
 - - * owner/owner_group
 - - *
 - - * Note that this can be a bit tricky as we might pass
 - - * a "name@domain" string to the server, but get a numeric
 - - * uid/gid as strings back, which means the name cache
 - - * might have both representations.
 - - */
 - - if (info->attrmask.arr[1] & FATTR4_WORD1_OWNER) {
 - - attr_request.count = 2;
 - - attr_request.arr[1] |= FATTR4_WORD1_OWNER;
 - - }
 - - if (info->attrmask.arr[1] & FATTR4_WORD1_OWNER_GROUP) {
 - - attr_request.count = 2;
 - - attr_request.arr[1] |= FATTR4_WORD1_OWNER_GROUP;
 - - }
 - + /*
 - + * If we set owner, owner_group and/or ACLs make sure we ask
 - + * the server to send the values back for two reasons:
 - + * 1, Handle cases like "all_squash" (e.g. nfsd turns
 - + * owner/owner_group always into "nobody"/"nogroup")
 - + * 2. Make sure we update the name cache with the new
 - + * owner/owner_group/ACLs
 - + *
 - + * Note that this can be a bit tricky as we might pass
 - + * a "name@domain" string to the server, but get a numeric
 - + * uid/gid as strings back, which means the name cache
 - + * might have both representations.
 - + */
 - + if (bitmap_isset(&info->attrmask, 0, FATTR4_WORD0_ACL)) {
 - + attr_request.count = __max(attr_request.count, 1);
 - + attr_request.arr[0] |= FATTR4_WORD0_ACL;
 - + }
 - + if (bitmap_isset(&info->attrmask, 1, FATTR4_WORD1_OWNER)) {
 - + attr_request.count = __max(attr_request.count, 2);
 - + attr_request.arr[1] |= FATTR4_WORD1_OWNER;
 - + }
 - + if (bitmap_isset(&info->attrmask, 1, FATTR4_WORD1_OWNER_GROUP)) {
 - + attr_request.count = __max(attr_request.count, 2);
 - + attr_request.arr[1] |= FATTR4_WORD1_OWNER_GROUP;
 - }
 - compound_add_op(&compound, OP_GETATTR, &getattr_args, &getattr_res);
 - diff --git a/daemon/nfs41_ops.h b/daemon/nfs41_ops.h
 - index f57f8dc..91cb766 100644
 - --- a/daemon/nfs41_ops.h
 - +++ b/daemon/nfs41_ops.h
 - @@ -1331,6 +1331,7 @@ int nfs41_superblock_getattr(
 - int nfs41_cached_getattr(
 - IN nfs41_session *session,
 - IN nfs41_path_fh *file,
 - + IN OPTIONAL bitmap4 *extra_attr_request,
 - OUT nfs41_file_info *info);
 - int nfs41_remove(
 - diff --git a/daemon/nfs41_xdr.c b/daemon/nfs41_xdr.c
 - index 4ccbeaa..d9e3629 100644
 - --- a/daemon/nfs41_xdr.c
 - +++ b/daemon/nfs41_xdr.c
 - @@ -1779,6 +1779,9 @@ static bool_t decode_file_attrs(
 - }
 - if (attrs->attrmask.arr[0] & FATTR4_WORD0_ACL) {
 - nfsacl41 *acl = info->acl;
 - +
 - + EASSERT(acl != NULL);
 - +
 - if (!xdr_array(xdr, (char**)&acl->aces, &acl->count,
 - NFS41_ACL_MAX_ACE_ENTRIES, sizeof(nfsace4),
 - (xdrproc_t)xdr_nfsace4))
 - diff --git a/daemon/open.c b/daemon/open.c
 - index aeaae11..2ba788a 100644
 - --- a/daemon/open.c
 - +++ b/daemon/open.c
 - @@ -867,7 +867,7 @@ static int handle_open(void *daemon_context, nfs41_upcall *upcall)
 - if (args->create_opts & FILE_OPEN_REPARSE_POINT) {
 - /* continue and open the symlink itself, but we need to
 - * know if the target is a regular file or directory */
 - - nfs41_file_info target_info;
 - + nfs41_file_info target_info = { 0 };
 - int target_status = nfs41_symlink_follow(upcall->root_ref,
 - state->session, &state->file, &target_info);
 - if (target_status == NO_ERROR) {
 - diff --git a/daemon/readdir.c b/daemon/readdir.c
 - index f3564bf..a754197 100644
 - --- a/daemon/readdir.c
 - +++ b/daemon/readdir.c
 - @@ -515,7 +515,7 @@ static int lookup_symlink(
 - {
 - nfs41_abs_path path;
 - nfs41_path_fh file;
 - - nfs41_file_info info;
 - + nfs41_file_info info = { 0 };
 - int status;
 - status = format_abs_path(parent->path, name, &path);
 - @@ -686,7 +686,7 @@ static int readdir_add_dots(
 - ZeroMemory(&entry->attr_info, sizeof(nfs41_file_info));
 - status = nfs41_cached_getattr(state->session,
 - - &state->file, &entry->attr_info);
 - + &state->file, NULL, &entry->attr_info);
 - if (status) {
 - DPRINTF(0, ("readdir_add_dots: failed to add '.' entry.\n"));
 - goto out;
 - @@ -719,7 +719,7 @@ static int readdir_add_dots(
 - ZeroMemory(&entry->attr_info, sizeof(nfs41_file_info));
 - status = nfs41_cached_getattr(state->session,
 - - &state->parent, &entry->attr_info);
 - + &state->parent, NULL, &entry->attr_info);
 - if (status) {
 - status = ERROR_FILE_NOT_FOUND;
 - DPRINTF(0, ("readdir_add_dots: failed to add '..' entry.\n"));
 - diff --git a/daemon/setattr.c b/daemon/setattr.c
 - index 1e2bfdd..a994548 100644
 - --- a/daemon/setattr.c
 - +++ b/daemon/setattr.c
 - @@ -71,7 +71,7 @@ static int handle_nfs41_setattr_basicinfo(void *daemon_context, setattr_upcall_a
 - (void)memset(&old_info, 0, sizeof(old_info));
 - getattr_status = nfs41_cached_getattr(state->session,
 - - &state->file, &old_info);
 - + &state->file, NULL, &old_info);
 - if (getattr_status) {
 - DPRINTF(0, ("handle_nfs41_setattr_basicinfo(args->path='%s'): "
 - "nfs41_cached_getattr() failed with error %d.\n",
 - diff --git a/daemon/util.c b/daemon/util.c
 - index 3a56b7c..535bad7 100644
 - --- a/daemon/util.c
 - +++ b/daemon/util.c
 - @@ -638,3 +638,21 @@ bool getwinntversionnnumbers(
 - return false;
 - #endif /* _WIN64 */
 - }
 - +
 - +bool dup_nfsacl41aces(
 - + nfsacl41 *restrict out_acl,
 - + const nfsacl41 *restrict in_acl)
 - +{
 - + size_t acemem_size = in_acl->count * sizeof(nfsace4);
 - +
 - + out_acl->aces = wintirpc_mem_alloc(acemem_size);
 - + if (out_acl->aces == NULL) {
 - + return false;
 - + }
 - +
 - + out_acl->count = in_acl->count;
 - + out_acl->flag = in_acl->flag;
 - + (void)memcpy(out_acl->aces, in_acl->aces, acemem_size);
 - +
 - + return true;
 - +}
 - diff --git a/daemon/util.h b/daemon/util.h
 - index 6938372..9c558af 100644
 - --- a/daemon/util.h
 - +++ b/daemon/util.h
 - @@ -376,4 +376,6 @@ bool_t waitcriticalsection(LPCRITICAL_SECTION cs);
 - bool getwinntversionnnumbers(DWORD *MajorVersionPtr, DWORD *MinorVersionPtr, DWORD *BuildNumberPtr);
 - +bool dup_nfsacl41aces(nfsacl41 *restrict out_acl, const nfsacl41 *restrict in_acl);
 - +
 - #endif /* !__NFS41_DAEMON_UTIL_H__ */
 - diff --git a/include/nfs41_driver.h b/include/nfs41_driver.h
 - index e11ebf5..06e8cfa 100644
 - --- a/include/nfs41_driver.h
 - +++ b/include/nfs41_driver.h
 - @@ -127,6 +127,8 @@ typedef enum _nfs41_start_driver_state {
 - #define NFS_VERSION_AUTONEGOTIATION (0xFFFF)
 - +/* Cache NFSv4.1 ACLs for max. 20 seconds */
 - +#define NFS41_ACL_CACHE_EXPIRATION (20)
 - /*
 - * LargeInteger.QuadPart value to indicate a time value was not
 - * available
 - diff --git a/sys/nfs41sys_acl.c b/sys/nfs41sys_acl.c
 - index c468433..f7437a4 100644
 - --- a/sys/nfs41sys_acl.c
 - +++ b/sys/nfs41sys_acl.c
 - @@ -235,7 +235,8 @@ NTSTATUS nfs41_QuerySecurityInformation(
 - (long long)current_time.QuadPart,
 - (long long)nfs41_fobx->time.QuadPart);
 - #endif
 - - if (current_time.QuadPart - nfs41_fobx->time.QuadPart <= 20*1000) {
 - + if ((current_time.QuadPart - nfs41_fobx->time.QuadPart) <=
 - + (NFS41_ACL_CACHE_EXPIRATION*1000)) {
 - PSECURITY_DESCRIPTOR sec_desc = (PSECURITY_DESCRIPTOR)
 - RxContext->CurrentIrp->UserBuffer;
 - RtlCopyMemory(sec_desc, nfs41_fobx->acl, nfs41_fobx->acl_len);
 
ACL support in name cache
Posted by Anonymous on Wed 25th Jun 2025 18:30
raw | new post
modification of post by Anonymous (view diff)
Submit a correction or amendment below (click here to make a fresh posting)
After submitting an amendment, you'll be able to view the differences between the old and new posts easily.
 nrubsig.kpaste.net RSS