[PATCH] audit: remove unnecessary curly braces from switch/case statements
by Paul Moore
From: Paul Moore <paul(a)paul-moore.com>
Signed-off-by: Paul Moore <paul(a)paul-moore.com>
---
kernel/auditsc.c | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/kernel/auditsc.c b/kernel/auditsc.c
index bde3aac4deed..4db32e8669f8 100644
--- a/kernel/auditsc.c
+++ b/kernel/auditsc.c
@@ -1221,7 +1221,7 @@ static void show_special(struct audit_context *context, int *call_panic)
context->ipc.perm_mode);
}
break; }
- case AUDIT_MQ_OPEN: {
+ case AUDIT_MQ_OPEN:
audit_log_format(ab,
"oflag=0x%x mode=%#ho mq_flags=0x%lx mq_maxmsg=%ld "
"mq_msgsize=%ld mq_curmsgs=%ld",
@@ -1230,8 +1230,8 @@ static void show_special(struct audit_context *context, int *call_panic)
context->mq_open.attr.mq_maxmsg,
context->mq_open.attr.mq_msgsize,
context->mq_open.attr.mq_curmsgs);
- break; }
- case AUDIT_MQ_SENDRECV: {
+ break;
+ case AUDIT_MQ_SENDRECV:
audit_log_format(ab,
"mqdes=%d msg_len=%zd msg_prio=%u "
"abs_timeout_sec=%ld abs_timeout_nsec=%ld",
@@ -1240,12 +1240,12 @@ static void show_special(struct audit_context *context, int *call_panic)
context->mq_sendrecv.msg_prio,
context->mq_sendrecv.abs_timeout.tv_sec,
context->mq_sendrecv.abs_timeout.tv_nsec);
- break; }
- case AUDIT_MQ_NOTIFY: {
+ break;
+ case AUDIT_MQ_NOTIFY:
audit_log_format(ab, "mqdes=%d sigev_signo=%d",
context->mq_notify.mqdes,
context->mq_notify.sigev_signo);
- break; }
+ break;
case AUDIT_MQ_GETSETATTR: {
struct mq_attr *attr = &context->mq_getsetattr.mqstat;
audit_log_format(ab,
@@ -1255,19 +1255,19 @@ static void show_special(struct audit_context *context, int *call_panic)
attr->mq_flags, attr->mq_maxmsg,
attr->mq_msgsize, attr->mq_curmsgs);
break; }
- case AUDIT_CAPSET: {
+ case AUDIT_CAPSET:
audit_log_format(ab, "pid=%d", context->capset.pid);
audit_log_cap(ab, "cap_pi", &context->capset.cap.inheritable);
audit_log_cap(ab, "cap_pp", &context->capset.cap.permitted);
audit_log_cap(ab, "cap_pe", &context->capset.cap.effective);
- break; }
- case AUDIT_MMAP: {
+ break;
+ case AUDIT_MMAP:
audit_log_format(ab, "fd=%d flags=0x%x", context->mmap.fd,
context->mmap.flags);
- break; }
- case AUDIT_EXECVE: {
+ break;
+ case AUDIT_EXECVE:
audit_log_execve_info(context, &ab);
- break; }
+ break;
case AUDIT_KERN_MODULE:
audit_log_format(ab, "name=");
audit_log_untrustedstring(ab, context->module.name);
7 years, 8 months
audit normalizer
by Steve Grubb
Hello,
The audit user space package has gained some real interesting features during
the 2.7.x releases. The events can now be normalized. So, what exactly does
that mean?
Events are composed of subject, action, object, and results. With the format
of the audit events, it can be hard for the unintiated to really tell what's
where with all the name=value fields and multi-lined events. What the
normalizer does is takes all that guess work out of interpreting the event.
Its presents an API in auparse that you can use to say, give me the subject,
give me the action, give me the results, etc.
The upshot of this is that you can use this to turn events into English
sentences. For example, this:
time->Mon Feb 13 10:09:04 2017
type=PROCTITLE msg=audit(1486998544.895:837):
proctitle=2F7573722F62696E2F696E7374616C6C002D6300636F6E66746573742E6F6E6500636F6E66746573742E74776F002F686F6D652F7367727562622F776F726B696E672F4255494C442F61756469742F636F6E66746573742E646972
type=PATH msg=audit(1486998544.895:837): item=0 name="/etc/selinux/config"
inode=17041117 dev=08:32 mode=0100600 ouid=0 ogid=0 rdev=00:00
obj=system_u:object_r:selinux_config_t:s0 nametype=NORMAL
type=CWD msg=audit(1486998544.895:837): cwd="/home/sgrubb/working/BUILD/audit"
type=SYSCALL msg=audit(1486998544.895:837): arch=c000003e syscall=2 success=no
exit=-13 a0=7fb05b8d5b8b a1=0 a2=1b6 a3=0 items=1 ppid=30491 pid=30650
auid=4325 uid=4325 gid=4325 euid=4325 suid=4325 fsuid=4325 egid=4325 sgid=4325
fsgid=4325 tty=pts3 ses=4 comm="install" exe="/usr/bin/install"
subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key="access"
Becomes:
At 10:09:04 02/13/2017 sgrubb unsuccessfully opened-file /etc/selinux/config
using /usr/bin/install
Big difference? Try it yourself. ausearch --start today --format text
But wait...there's more!!! ausearch can now also output events as a comma
separated file (CSV) format. What this can do for you is open the whole world
to high quality visualizations of audit events. You can do this:
ausearch --start today --format csv > audit.csv
Then you can open the file with libreoffice if you like:
ooffice audit.csv
Review the sample import and adjust or Click on OK when it asks. Then you
should see the audit data in nice neat columns with one event per row. If you
like using spreadsheets to do charts and graphs, have at it.
Or, you can close the spreadsheet and visit here:
http://app.rawgraphs.io/
Open your csv file in gedit or something and select all rows and copy to the
clipboard. The paste your data into the box at app.rawgraphs.io. Then select
alluvial diagram. Then scroll down to "map your dimensions". Grab "subj_prime"
and drag it to the box labeled "steps". Then grab "event_kind" and drag it
under "subj_prime". Then grab "action" and drag it under "event_kind". This
shows who is doing what kind of things on the system.
If you wanted to see what login accounts transition to other accounts, delete
the green boxes in the "steps" section and grab "subj_prime" and drag it to
the "steps". Then grab "subj_sec" and drag it under "subj_prime". There's your
chart. Its that easy.
There are a few things that say "unknown". This is caused by malformed events
that we are still working to correct. Feel free to experiment. You can't
really break anything.
As mentioned before, I will be starting up a blog to explain how to use the R
programming langauge to create interesting reports. With the logs normalized,
we can now use Data Science tools to look at logs. That opens a whole lot of
doors.
-Steve
7 years, 8 months
audit 2.7.2 released
by Steve Grubb
Hello,
I've just released a new version of the audit daemon. It can be downloaded
from http://people.redhat.com/sgrubb/audit. It will also be in rawhide
soon. The ChangeLog is:
- Rename whole auparse classifier subsystem to normalizer
- Add documentation about networking and systemd
- Adjust text in auparse normalizer
- In ausearch, fix parsing of kernel anomaly events
- Add filesystem object to the auparse normalizer
- Add basic support for formatted output in ausearch
- Add 'extra' options for csv output in ausearch
- Add event kind metadata to the auparse normalizer
- Add event kind metadata to the ausearch csv format
- Add auparse normalizer support to some anomaly events
- In libaudit logging functions, fill in hostname if we have real tty
- Add new virtualization events
- Fix compile time feature detection in auditctl
In the 2.7.x releases is a big new feature that I have not talked very much
about. Starting with this release I'll start talking about it. The audit logs
can now be normalized. This means we can do lots of new things around
analytics. So much so, that I will send a separate email discussing this new
feature. I'll also start posting to a blog to explain all the things that you
can now do. If you have the ability to compile the sources, do it and try
ausearch --start today --format text
Besides this, the release fixes a bug in parsing of kernel anaomaly events for
ausearch/report and we added types for some new virtualization events.
I will try to get a 2.7.3 release out in a little under 2 weeks. This is to
get one last release off of the svn site before it goes away. Testing and
feedback around the normalizer would be greatly appreciated. As mentioned,
I'll start another thread to discuss it.
Please let me know if you run across any problems with this release.
-Steve
7 years, 8 months
Question concerning -l option
by Tom Hall
Please forgive me, I assume this has already been addressed in the mail archive but I've been unable to locate a related thread. Can someone tell me why the default for auditd is O_NOFOLLOW for accessing auditd configuration files? I assume there is a reason for not supporting links as the default that is important enough to justify the extra work to add the -l option but it is not clear to me.
Thanks,
Tom Hall
Brocade Communications Systems, Inc.
7 years, 8 months
AUDIT_FEATURE_VERSION and AUDIT_FEATURE_BITMAP
by Richard Guy Briggs
Hi Steve,
I'd rather have filed an issue on github linux-audit/audit-userspace,
but I know you don't like using it. I didn't want to lose track of this
issue.
Looking through the userspace audit code when trying to figure out why
--reset-lost wasn't working on RHEL7, I came across a compiler directive
that was used a number of times and I don't understand why.
In particular, in lib/libaudit.c, lib/netlink.c, src/auditctl-listing.c,
src/auditctl.c, I see:
#if defined(HAVE_DECL_AUDIT_FEATURE_VERSION) && \
defined(HAVE_STRUCT_AUDIT_STATUS_FEATURE_BITMAP)
used together which does not make sense since they are unrelated.
The AUDIT_FEATURE_BITMAP has *nothing* to do with AUDIT_FEATURE_VERSION.
This naming was short-sighted in retrospect.
AUDIT_SET_FEATURE (audit_set_feature()), AUDIT_GET_FEATURE
(audit_request_features()) and AUDIT_FEATURE_LOGINID_IMMUTABLE (and
unused AUDIT_FEATURE_ONLY_UNSET_LOGINUID) are related and present when
AUDIT_FEATURE_VERSION is present and positive. They allow a kernel
feature named in audit_feature_names[] to be turned off or oon and
unlocked or locked.
AUDIT_VERSION_* (deprecated), AUDIT_FEATURE_BITMAP_* along with the
struct audit_status.feature_bitmap (STRUCT_AUDIT_STATUS_FEATURE_BITMAP)
are used to simply determine if the kernel supports such a feature,
extracted by audit_get_features() via load_feature_bitmap() and stored in
features_bitmap (AUDIT_FEATURES_UNSET, AUDIT_FEATURES_UNSUPPORTED).
Most (if not all) of the uses of the compiler directive above should be
just the first half, HAVE_DECL_AUDIT_FEATURE_VERSION.
The use in lib/libaudit.h of AUDIT_FEATURE_BITMAP_ALL in struct
audit_reply->features should instead be HAVE_DECL_AUDIT_FEATURE_VERSION.
- RGB
--
Richard Guy Briggs <rgb(a)redhat.com>
Kernel Security Engineering, Base Operating Systems, Red Hat
Remote, Ottawa, Canada
Voice: +1.647.777.2635, Internal: (81) 32635
7 years, 8 months
[PATCH] auditctl: allow reset-lost to be available when backlog_wait_time isn't
by Richard Guy Briggs
When HAVE_DECL_AUDIT_VERSION_BACKLOG_WAIT_TIME is not available the
--reset-lost command line option is not available despite it being advertised
in the help text.
Enable it if FEATURE_BITMAP features are available.
This is to support:
https://github.com/linux-audit/audit-kernel/issues/3
https://bugzilla.redhat.com/show_bug.cgi?id=1249813
Signed-off-by: Richard Guy Briggs <rgb(a)redhat.com>
---
trunk/src/auditctl.c | 2 ++
1 files changed, 2 insertions(+), 0 deletions(-)
diff --git a/trunk/src/auditctl.c b/trunk/src/auditctl.c
index 81000ee..dfcfae6 100644
--- a/trunk/src/auditctl.c
+++ b/trunk/src/auditctl.c
@@ -514,6 +514,8 @@ struct option long_opts[] =
#endif
#if HAVE_DECL_AUDIT_VERSION_BACKLOG_WAIT_TIME
{"backlog_wait_time", 1, NULL, 2},
+#endif
+#if defined(HAVE_STRUCT_AUDIT_STATUS_FEATURE_BITMAP)
{"reset-lost", 0, NULL, 3},
#endif
{NULL, 0, NULL, 0}
--
1.7.1
7 years, 8 months
[PATCH V4] audit: add feature audit_lost reset
by Richard Guy Briggs
Add a method to reset the audit_lost value.
An AUDIT_SET message with the AUDIT_STATUS_LOST flag set by itself
will return a positive value repesenting the current audit_lost value
and reset the counter to zero. If AUDIT_STATUS_LOST is not the
only flag set, the reset command will be ignored. The value sent with
the command is ignored. The return value will be the +ve lost value at
reset time.
An AUDIT_CONFIG_CHANGE message will be queued to the listening audit
daemon. The message will be a standard CONFIG_CHANGE message with the
fields "lost=0" and "old=" with the latter containing the value of
audit_lost at reset time.
See: https://github.com/linux-audit/audit-kernel/issues/3
Signed-off-by: Richard Guy Briggs <rgb(a)redhat.com>
---
There is a merge conflict anticipated with the exclude filter
FEATURE_BITMAP patch (ghak5)
v2:
Switch from AUDIT_GET to AUDIT_SET
Remove AUDIT_FEATURE and AUDIT_FEATURE_BITMAP
Return +ve lost value, reply AUDIT_LOST_RESET msg to sender
v3:
Switch, from reply to sender, to queue to audit log
v4:
Switch from LOST_RESET to CONFIG_CHANGE log msg
Re-add AUDIT_FEATURE_BITMASK
---
---
include/uapi/linux/audit.h | 6 +++++-
kernel/audit.c | 8 +++++++-
2 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/include/uapi/linux/audit.h b/include/uapi/linux/audit.h
index c8dc97b..3f24110 100644
--- a/include/uapi/linux/audit.h
+++ b/include/uapi/linux/audit.h
@@ -326,15 +326,19 @@ enum {
#define AUDIT_STATUS_RATE_LIMIT 0x0008
#define AUDIT_STATUS_BACKLOG_LIMIT 0x0010
#define AUDIT_STATUS_BACKLOG_WAIT_TIME 0x0020
+#define AUDIT_STATUS_LOST 0x0040
#define AUDIT_FEATURE_BITMAP_BACKLOG_LIMIT 0x00000001
#define AUDIT_FEATURE_BITMAP_BACKLOG_WAIT_TIME 0x00000002
#define AUDIT_FEATURE_BITMAP_EXECUTABLE_PATH 0x00000004
#define AUDIT_FEATURE_BITMAP_SESSIONID_FILTER 0x00000010
+#define AUDIT_FEATURE_BITMAP_LOST_RESET 0x00000020
+
#define AUDIT_FEATURE_BITMAP_ALL (AUDIT_FEATURE_BITMAP_BACKLOG_LIMIT | \
AUDIT_FEATURE_BITMAP_BACKLOG_WAIT_TIME | \
AUDIT_FEATURE_BITMAP_EXECUTABLE_PATH | \
- AUDIT_FEATURE_BITMAP_SESSIONID_FILTER)
+ AUDIT_FEATURE_BITMAP_SESSIONID_FILTER | \
+ AUDIT_FEATURE_BITMAP_LOST_RESET)
/* deprecated: AUDIT_VERSION_* */
#define AUDIT_VERSION_LATEST AUDIT_FEATURE_BITMAP_ALL
diff --git a/kernel/audit.c b/kernel/audit.c
index 57acf25..25dd70a 100644
--- a/kernel/audit.c
+++ b/kernel/audit.c
@@ -121,7 +121,7 @@ u32 audit_sig_sid = 0;
3) suppressed due to audit_rate_limit
4) suppressed due to audit_backlog_limit
*/
-static atomic_t audit_lost = ATOMIC_INIT(0);
+static atomic_t audit_lost = ATOMIC_INIT(0);
/* The netlink socket. */
static struct sock *audit_sock;
@@ -1052,6 +1052,12 @@ static int audit_receive_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
if (err < 0)
return err;
}
+ if (s.mask == AUDIT_STATUS_LOST) {
+ u32 lost = atomic_xchg(&audit_lost, 0);
+
+ audit_log_config_change("lost", 0, lost, 1);
+ return lost;
+ }
break;
}
case AUDIT_GET_FEATURE:
--
1.7.1
7 years, 8 months
[RFC PATCH] audit: normalize NETFILTER_PKT
by Richard Guy Briggs
Eliminate flipping in and out of message fields.
https://github.com/linux-audit/audit-kernel/issues/11
Signed-off-by: Richard Guy Briggs <rgb(a)redhat.com>
---
net/netfilter/xt_AUDIT.c | 92 +++++++++++++++++++++++++++++++++-------------
1 files changed, 66 insertions(+), 26 deletions(-)
diff --git a/net/netfilter/xt_AUDIT.c b/net/netfilter/xt_AUDIT.c
index 4973cbd..8089ec2 100644
--- a/net/netfilter/xt_AUDIT.c
+++ b/net/netfilter/xt_AUDIT.c
@@ -31,24 +31,41 @@ MODULE_ALIAS("ip6t_AUDIT");
MODULE_ALIAS("ebt_AUDIT");
MODULE_ALIAS("arpt_AUDIT");
+struct nfpkt_par {
+ int ipv;
+ int iptrunc;
+ const void *saddr;
+ const void *daddr;
+ u16 ipid;
+ u8 proto;
+ u8 frag;
+ int ptrunc;
+ u16 sport;
+ u16 dport;
+ u8 icmpt;
+ u8 icmpc;
+};
+
static void audit_proto(struct audit_buffer *ab, struct sk_buff *skb,
- unsigned int proto, unsigned int offset)
+ unsigned int proto, unsigned int offset, struct nfpkt_par *apar)
{
switch (proto) {
case IPPROTO_TCP:
case IPPROTO_UDP:
- case IPPROTO_UDPLITE: {
+ case IPPROTO_UDPLITE:
+ case IPPROTO_DCCP:
+ case IPPROTO_SCTP: {
const __be16 *pptr;
__be16 _ports[2];
pptr = skb_header_pointer(skb, offset, sizeof(_ports), _ports);
if (pptr == NULL) {
- audit_log_format(ab, " truncated=1");
+ apar->ptrunc = 1;
return;
}
+ apar->sport = ntohs(pptr[0]);
+ apar->dport = ntohs(pptr[1]);
- audit_log_format(ab, " sport=%hu dport=%hu",
- ntohs(pptr[0]), ntohs(pptr[1]));
}
break;
@@ -59,41 +76,43 @@ static void audit_proto(struct audit_buffer *ab, struct sk_buff *skb,
iptr = skb_header_pointer(skb, offset, sizeof(_ih), &_ih);
if (iptr == NULL) {
- audit_log_format(ab, " truncated=1");
+ apar->ptrunc = 1;
return;
}
-
- audit_log_format(ab, " icmptype=%hhu icmpcode=%hhu",
- iptr[0], iptr[1]);
+ apar->icmpt = iptr[0];
+ apar->icmpc = iptr[1];
}
break;
}
}
-static void audit_ip4(struct audit_buffer *ab, struct sk_buff *skb)
+static void audit_ip4(struct audit_buffer *ab, struct sk_buff *skb, struct nfpkt_par *apar)
{
struct iphdr _iph;
const struct iphdr *ih;
+ apar->ipv = 4;
ih = skb_header_pointer(skb, 0, sizeof(_iph), &_iph);
if (!ih) {
- audit_log_format(ab, " truncated=1");
+ apar->iptrunc = 1;
return;
}
- audit_log_format(ab, " saddr=%pI4 daddr=%pI4 ipid=%hu proto=%hhu",
- &ih->saddr, &ih->daddr, ntohs(ih->id), ih->protocol);
+ apar->saddr = &ih->saddr;
+ apar->daddr = &ih->daddr;
+ apar->ipid = ntohs(ih->id);
+ apar->proto = ih->protocol;
if (ntohs(ih->frag_off) & IP_OFFSET) {
- audit_log_format(ab, " frag=1");
+ apar->frag = 1;
return;
}
- audit_proto(ab, skb, ih->protocol, ih->ihl * 4);
+ audit_proto(ab, skb, ih->protocol, ih->ihl * 4, apar);
}
-static void audit_ip6(struct audit_buffer *ab, struct sk_buff *skb)
+static void audit_ip6(struct audit_buffer *ab, struct sk_buff *skb, struct nfpkt_par *apar)
{
struct ipv6hdr _ip6h;
const struct ipv6hdr *ih;
@@ -101,9 +120,10 @@ static void audit_ip6(struct audit_buffer *ab, struct sk_buff *skb)
__be16 frag_off;
int offset;
+ apar->ipv = 6;
ih = skb_header_pointer(skb, skb_network_offset(skb), sizeof(_ip6h), &_ip6h);
if (!ih) {
- audit_log_format(ab, " truncated=1");
+ apar->iptrunc = 1;
return;
}
@@ -111,11 +131,12 @@ static void audit_ip6(struct audit_buffer *ab, struct sk_buff *skb)
offset = ipv6_skip_exthdr(skb, skb_network_offset(skb) + sizeof(_ip6h),
&nexthdr, &frag_off);
- audit_log_format(ab, " saddr=%pI6c daddr=%pI6c proto=%hhu",
- &ih->saddr, &ih->daddr, nexthdr);
+ apar->saddr = &ih->saddr;
+ apar->daddr = &ih->daddr;
+ apar->proto = nexthdr;
if (offset)
- audit_proto(ab, skb, nexthdr, offset);
+ audit_proto(ab, skb, nexthdr, offset, apar);
}
static unsigned int
@@ -123,6 +144,9 @@ audit_tg(struct sk_buff *skb, const struct xt_action_param *par)
{
const struct xt_audit_info *info = par->targinfo;
struct audit_buffer *ab;
+ struct nfpkt_par apar = {
+ -1, -1, NULL, NULL, -1, -1, -1, -1, -1, -1, -1, -1
+ };
if (audit_enabled == 0)
goto errout;
@@ -136,8 +160,7 @@ audit_tg(struct sk_buff *skb, const struct xt_action_param *par)
par->in ? par->in->name : "?",
par->out ? par->out->name : "?");
- if (skb->mark)
- audit_log_format(ab, " mark=%#x", skb->mark);
+ audit_log_format(ab, " mark=%#x", skb->mark ?: -1);
if (skb->dev && skb->dev->type == ARPHRD_ETHER) {
audit_log_format(ab, " smac=%pM dmac=%pM macproto=0x%04x",
@@ -147,25 +170,42 @@ audit_tg(struct sk_buff *skb, const struct xt_action_param *par)
if (par->family == NFPROTO_BRIDGE) {
switch (eth_hdr(skb)->h_proto) {
case htons(ETH_P_IP):
- audit_ip4(ab, skb);
+ audit_ip4(ab, skb, &apar);
break;
case htons(ETH_P_IPV6):
- audit_ip6(ab, skb);
+ audit_ip6(ab, skb, &apar);
break;
}
}
+ } else {
+ audit_log_format(ab, " smac=? dmac=? macproto=0xffff");
}
switch (par->family) {
case NFPROTO_IPV4:
- audit_ip4(ab, skb);
+ audit_ip4(ab, skb, &apar);
break;
case NFPROTO_IPV6:
- audit_ip6(ab, skb);
+ audit_ip6(ab, skb, &apar);
+ break;
+ }
+
+ switch (apar.ipv) {
+ case 4:
+ audit_log_format(ab, " trunc=%d saddr=%pI4 daddr=%pI4 ipid=%hu proto=%hhu frag=%d",
+ apar.iptrunc, apar.saddr, apar.daddr, apar.ipid, apar.proto, apar.frag);
+ break;
+ case 6:
+ audit_log_format(ab, " trunc=%d saddr=%pI6c daddr=%pI6c ipid=-1 proto=%hhu frag=-1",
+ apar.iptrunc, apar.saddr, apar.daddr, apar.proto);
break;
+ default:
+ audit_log_format(ab, " trunc=-1 saddr=? daddr=? ipid=-1 proto=-1 frag=-1");
}
+ audit_log_format(ab, " trunc=%d sport=%hu dport=%hu icmptype=%hhu icmpcode=%hhu",
+ apar.ptrunc, apar.sport, apar.dport, apar.icmpt, apar.icmpc);
#ifdef CONFIG_NETWORK_SECMARK
if (skb->secmark)
--
1.7.1
7 years, 8 months
auditd restart atomic?
by Chris Nandor
If I restart auditd, can it lose (not record to the logs) events that
happen during the restart? Or is the restart (and reload of new rules)
essentially atomic?
Thanks,
--Chris
7 years, 8 months