Search Results: "ema"

20 October 2010

Steve McIntyre: YA spam vector?

Just had an email from twitter.com telling me that my new account was ready for use. Looks like some spammers are trying to abuse people that way too. Simon said he'd got one too. Sigh.

Julian Andres Klode: The Three Levels of Package Management

In today s Linux distributions, there are usually two to three levels of package management. In this blog post, I will explain the three levels of package management. 1. Local (dpkg, rpm) The first level of package management is the local level. This level consists of package management tools that install and/or remove packages via package archives such as .deb or .rpm files and a database of the local s system state. 2. Connected (APT, YUM, Smart) The second level of package management is the connected level. In this level, tools are fetching packages and information about them from (remote) locations known as repositories or sources . A level 2 package manager is usually built on top of a level 1 package manager, for example, APT uses dpkg but there are also cases where one tool is both, such as opkg. 3. Abstract (PackageKit, Smart) A level 3 package manager is a tool that supports different level 1 package managers via one generic interface. It may be implemented on top of a level 2 package manager (PackageKit), or may be implemented directly in level 2 (Smart). Conclusion: Merge Level 2 and 3 Most level 2 package managers share a great deal of tasks, such as solving dependency problems. Merging level 2 package management and level 3 package management would enable us to reduce the number of dependency problem solvers and combine our efforts into a few package managers. Such an implementation would need to be written in C and support all common level 1 package managers in order to be successful. As some might have guessed, this is what I plan to do with the project codenamed APT2, although work is not progressing very fast. PS: Back from vacation I spent the more than the complete last week in (mostly rainy) Corfu, Greece and am now getting back to my usual development stuff. I have already processed my news & blogs backlog (1000+ messages) and will now start with my email backlog, so don t be angry if the answer to your email takes some time.
Filed under: APT2, General

Julien Danjou: No more dashes in Emacs 24 mode-line

We all know the good old Emacs mode-line you got under every window. Since the beginning (a long time ago), it starts and ends with dashes. I've proposed a patch to remove them. Before: After: This has been merged in Emacs 24. You won't see any more ugly dashes in graphical mode. Flattr this

Michal Čihař: Another step in Gammu documentation

I've just fnished another step for new Gammu manual - it includes also documentation for gammu utility. IIt includes all content from the man page, reformatted and improved in some sections. So now the documentation does cover all important parts and can contain correct cross references. The only problematic thing was to handle non dash starting commands in Sphinx. Unfortunately it does not seem to like this idea, so I had to invent some ugly hacks, which workaround that (just changing the regexps which sphinx.domains.std uses). The man page will be from now on generated from this source as well.

Filed under: Gammu 0 comments Flattr this

19 October 2010

Kees Cook: CVE-2010-2963 v4l compat exploit

If you re running a 64bit system, and you ve got users with access to a video device (/dev/video*), then be sure you update your kernels for CVE-2010-2963. I ve been slowly making my way through auditing the many uses in the Linux kernel of the copy_from_user() function, and ran into this vulnerability. Here s the kernel code from drivers/media/video/v4l2-compat-ioctl32.c:
static int get_microcode32(struct video_code *kp, struct video_code32 __user *up)
 
        if (!access_ok(VERIFY_READ, up, sizeof(struct video_code32))  
                copy_from_user(kp->loadwhat, up->loadwhat, sizeof(up->loadwhat))  
                get_user(kp->datasize, &up->datasize)  
                copy_from_user(kp->data, up->data, up->datasize))
                        return -EFAULT;
        return 0;
 
Note that kp->data is being used as the target for up->data in the final copy_from_user() without actually verifying that kp->data is pointing anywhere safe. Here s the caller of get_microcode32:
static long do_video_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
 
        union  
                struct video_tuner vt;
                struct video_code vc;
...
          karg;
        void __user *up = compat_ptr(arg);
...
        switch (cmd)  
...
        case VIDIOCSMICROCODE:
                err = get_microcode32(&karg.vc, up);
...
So, the contents of up are totally under control of the caller, and the contents of karg (in our case, the video_code structure) are not initialized at all. So, it seems like a call for VIDIOCSMICROCODE would write video_code->datasize bytes from video_code->data into some random kernel address, just causing an Oops, since we don t control what is on the kernel s stack. But wait, who says we can t control the contents of the kernel s stack? In fact, this compat function makes it extremely easy. Let s look back at the union. Notice the struct video_tuner? That gets populated from the caller s up memory via this case of the switch (cmd) statement:
...
        case VIDIOCSTUNER:
        case VIDIOCGTUNER:
                err = get_video_tuner32(&karg.vt, up);
...
So, to control the kernel stack, we just need to call this ioctl twice in a row: once to populate the stack via VIDIOCSTUNER with the contents we want (including the future address for video_code->data, which starts at the same location as video_tuner->name[20]), and then again with VIDIOCSMICROCODE. Tricks involved here are: the definition of the VIDIOCSMICROCODE case in the kernel is wrong, and calling the ioctls without any preparation can trigger other kernel work (memory faults, etc) that may destroy the stack contents. First, we need the real value for the desired case statement. This turns out to be 0 4020761b. Next, we just repeatedly call the setup ioctl in an attempt to get incidental kernel work out of the way so that our last ioctl doing the stack preparation will stick, and then we call the buggy ioctl to trigger the vulnerability. Since the ioctl already does a multi-byte copy, we can now copy arbitrary lengths of bytes into kernel memory. One method of turning an arbitrary kernel memory write into a privilege escalation is to overwrite a kernel function pointer, and trigger that function. Based on the exploit for CVE-2010-3081, I opted to overwrite the security_ops function pointer table. Their use of msg_queue_msgctl wasn t very good for the general case since it s near the end of the table and its offset would depend on kernel versions. Initially I opted for getcap, but in the end used ptrace_traceme, both of which are very near the top the security_ops structure. (Though I need share credit here with Dan Rosenberg as we were working together on improving the reliability of the security_ops overwrite method. He used the same approach for his excellent RDS exploit.) Here are the steps for one way of taking an arbitrary kernel memory write and turning it into a root escalation: Here s the source for Vyakarana as seen running in Enlightenment using cap_getcap (which is pretty unstable, so you might want to switch it to use ptrace_traceme), and as a stand-alone memory writer. Conclusions: Keep auditing the kernel for more arbitrary writes; I think there are still many left. Reduce the exploitation surface within the kernel itself (which PaX and grsecurity have been doing for a while now), specifically:

Torsten Landschoff: Fun with my Sony PRS-600 eBook reader

I finally bought myself an eBook reader. To be precise, I got the Sony PRS-600 Reader Touch Edition. Two reasons: First of all, the display is only really great with a powerful light source. The reflection is a bit of a nuisance but I can live with it. What I don t like is the PDF support of the device. Viewing a normal page, the font faces are so tiny that it is not readable in any way. The zoom feature works around that, but you can t switch pages from there. Instead you have to unzoom, go to the next page and zoom again. Crappy! There is also the reflow option, which does not mix with source code figures in my books. I am glad that most of my electronic books are from The Pragmatic Bookshelf, which means that I can download them in Epub as well, all without DRM. Thanks, guys! For the PDFs I would like to carry, I googled a while and stumbled across this blog post. The example to work with source documents worked fine for The Little Book of Semaphores at least. But even with BRISS I did not (yet) manage to crop books I bought from APress. Most likely I will try doing it via a bit of python scripting and the poppler bindings for python. I also bought my first DRM content: Star Trek Vanguard: Harbinger. I really enjoyed the book, but the download was a PITA. After buying, you only get a link to a ASCM file, which is not useful by itself. One needs Adobe Digital Editions to download it. So I installed it into a virtual machine which has XP installed. After that, I was able to read it on the PC, which wasn t the goal of the exercise. To put it on the reader, one needs to authorize it from the same Windows software and upload the file. All that stuff took almost an hour and of course the digital edition of the book is more expensive than getting it from amazon germany. But at least I did not have to wait for delivery. Having finished the book I wanted to buy the second part, but of course it is only available for canadian residents on ebooks.com. I researched a number of ebook sites which mostly sell it only in the US. Funny enough, the first hits when searching where pirated versions of the book in full text. Why do they make it so hard to get stuff legally? *sigh*

Julien Danjou: Enhancing Emacs mouse avoidance

Recent Emacs versions have a wonderful capacity to hide the mouse pointer as soon as you type and insert characters in a buffer. This is controlled by the make-pointer-invisible' variable, which is set to t by default. However that does not hide the pointer when simply moving the cursor on screen. Therefore, I've started to use mouse-avoidance-mode', which make the mouse pointer jump if your cursor hits it. Unfortunately, if your cursor hits the invisible mouse pointer, mouse-avoidance-mode' makes it jump too, because it does not know it is invisible. Well, it did not know. Now it does, thanks to my patches which have been merged in Emacs 24. Using the new function frame-pointer-invisible-p', one can know if the mouse pointer has been hidden by Emacs. Therefore I enhanced mouse-avoidance-mode' to use it, and everything is alright now. :-) Flattr this

MJ Ray: FixTheWeb is Coming!

I travel across the Sedgemoor/North Somerset/Bristol council borders, so I m a big fan of FixMyStreet which puts fault reporting for things like potholes, flytipping and broken lights behind a common interface. Credit to the MySociety charity project, once again. So I m quite happy that something similar is starting for website faults. If you know disabled people who are willing to report web accessibility issues for volunteers to pick up, please let them know they can currently: They will very shortly be able to use a browser toolbar too, which should make things even easier. The core team are intending to launch a website later in November, but there are the usual teething problems.

18 October 2010

Erich Schubert: Blog moving

My blog will move soon to a new URL and software. In fact, I've written my own static HTML based blog generator, wrapping around Atom for data storage.This is why the feed will also be only available in Atom format in the future - it works a lot better, and why support 2 formats?The most important feeds are: The old URLs will be replaced by redirects anytime soon. If you notice anything being broken, please send me an email at the usual erich (AT) debian org address.

Erich Schubert: Blog moving

My blog will move soon to a new URL and software. In fact, I've written my own static HTML based blog generator, wrapping around Atom for data storage.This is why the feed will also be only available in Atom format in the future - it works a lot better, and why support 2 formats?The most important feeds are: The old URLs will be replaced by redirects anytime soon.If you notice anything being broken, please send me an email at the usual erich (AT) debian org address.

17 October 2010

Neil Williams: Migrating debconf settings

I've been trying to work out how to handle (or whether to handle) the vestigial debconf values from emdebian-tools which was replaced in Debian some months ago. I tried to transfer the postrm script to libemdebian-tools-perl which itself is currently a transitional package. Turns out that when debconf tries to purge libemdebian-tools-perl, it doesn't purge the old emdebian-tools settings (it looks for libemdebian-tools-perl.templates instead of emdebian-tools.templates) - unless those were themselves setup by libemdebian-tools-perl. i.e. users with the original emdebian-tools setup will likely have debconf settings that are not purged by removing the package to which those settings should have migrated.

I have reconstructed a bare templates file locally and a simple script that can purge those values and it works on my system - I'm just not sure if this should be implemented in what is now merely a transitional package (libemdebian-tools-perl) which exists merely to support migrating to the current emdebian-crush package.

It's only three fields - single word values - and debconf settings are only a cache after all. Comments are still disabled on my blog but my email address isn't hard to find. If people really think it's worth writing code to "catch-up" with these redundant settings, I can do something after Squeeze. What I don't want to do is reintroduce debconf templates which are no longer used, merely to remove old settings left behind from the first migration.

So, lazyweb, what's the feeling? Is this worth doing anything about?

16 October 2010

Tim Retout: Erm... more bugs.

I went shopping today, and inadvertently stole a very nice cardigan-like thing from a multinational clothing retailer. Enough anecdotes. Yesterday: Today: Looking at the weekly summary reports, we squashed a net total of 31 RC bugs last week. Of course, it's possible they'll get more difficult as we go along... but I'm hoping to see all the easy ones closed before the BSP two weeks from now.

Vincent Sanders: Coming to terms

Yesterday was not a great day. One of the family's cats died.

Molly just after she arrived 26th September 2000
Molly, for that was her name, came to us the week Melodie and I moved into our first house together. September 15th 2000, in the middle of the fuel protests which were raging at the time, we hired a van and moved in.

Most people would have considered that enough for one week! We happened to be in Halifax on the Sunday afternoon at the supermarket and decided to visit the RSPCA because somehow we decided what our new home needed was a cat.

On that day a decade ago we saw Molly and Lucy, a pair of cats who needed a new home as their previous owners had a baby who was allergic to them. I still recall them skulking at the back of their cage in the cattery, Lucy who was reluctant to come down of a perch to be petted and Molly looking generally unhappy. No-one else wanted this misfit pair because they were already both over 5 years old, the warden despairing of ever finding them a home.

Of course they were obviously the right animals for us! ;-) So by the end of the week we had taken them home. Molly immediately showed how things were going to be by shredding her way out of the RSPCA provided double walled cardboard carrier. To the day she died she detested being placed inside a carrier, funny that.

Melodie and Molly in the snow 28th December 2000
By the end of the year, when the snow came, the cats ruled the house and we were all happy together.

Little did we realise that soon in the summer of 2001 there would be another arrival to the family.

Molly with Melodie and Alex 2001
The arrival of our first child in June 2001 was a complete change in all our lives but we all managed to settle back into a routine. Although the banishment of the cats from upstairs remained a point of disagreement for a long time.

As Alex grew up the cats learned that feeding times for Alex could result in all manner of things falling from above. Soon Alex was mobile which resulted in different lessons on cats being sharp objects if not treated with respect. In February 2003 our family grew once more with the arrival of Joshua. The cats, now used to infants, took this in their stride.

Molly in Febuary 2004 hiding on a windowsill
While still about day to day the cats are not captured on camera so often form this point on.

Over the forthcoming months they ensured they were out of reach of the newborn and the precocious toddler.

By the time of Joshuas first birthday in 2004 Molly had taken to hiding "out of the way" as much as possible but remained as affectionate as ever.

Molly in a box March 2007
As the children grew up and life progressed Molly became ever more at home and developed the odd aggravating trait like taking clean washing off airers and dragging it out the window, through the cat flap and upstairs so she could sleep on it!

She still enjoyed participating in claiming boxes and defending them vigorously though!

The kids took on the job of feeding the cats which made their bond closer ensuring they were greeted by happy cats sitting lookout as they came home from school each day.

Molly continued to be a good companion and an infuriating self centred animal like all good cats.
Molly asleep in the kitchen sink
Then on Tuesday evening our Neighbour knocked on our front door. They gave us the news that one of our cats had been run over and taken to the Vet. I immediately went to the back door and called and shook the treat box.

Lucy came running, but molly did not. We rang the vets who confirmed they had molly (we had them electronically tagged back in Halifax) and that they advised we did not come and see her until they had chance to asses her and deal with the shock.

As the days progressed her prognosis improved and then sank. She had a broken jaw, broken teeth, a dislocated hip, extensive bruising and something was definitely wrong with her kidney function.

Thursday evening the whole family went to see her and she purred and seemed happy to see us, she looked as bad as I feared though and somehow I knew there and then that this was probably goodbye. The children stroked her and petted her for a while and we left with an odd sadness, oh and to the sound of molly trying to gouge the veterinary nurse.

Yesterday she was due to have surgery to fix her jaw and hip...Alas when she was anaesthetised and x-rayed once more it became evident the hip was not just dislocated but her femur was fractured and there was additional damage. So just after midday came the call to ask what we wanted to do. The vet could attempt the repairs but due to her age and the other complications it was probably futile. So with heavy heart we agreed the best thing was not to revive her and she died a short time later.

I shall miss her morning greetings, her demands for attention, her sleeping in odd places and her companionship. I keep calling for her at meal times forgetting she will never come. I think Lucy is upset too, after spending their lives together her friend is gone and I cannot explain that to her.

So that is the end of Molly, a good cat.


15 October 2010

Enrico Zini: Award winning code

Award winning code Me and Yuwei had a fun day at hhhmcr (#hhhmcr) and even managed to put together a prototype that won the first prize \o/ We played with the gmp24 dataset kindly extracted from Twitter by Michael Brunton-Spall of the Guardian into a convenient JSON dataset. The idea was to find ways of making it easier to look at the data and making sense of it. This is the story of what we did, including the code we wrote. The original dataset has several JSON files, so the first task was to put them all together:
#!/usr/bin/python
# Merge the JSON data
# (C) 2010 Enrico Zini <enrico@enricozini.org>
# License: WTFPL version 2 (http://sam.zoy.org/wtfpl/)
import simplejson
import os
res = []
for f in os.listdir("."):
    if not f.startswith("gmp24"): continue
    data = open(f).read().strip()
    if data == "[]": continue
    parsed = simplejson.loads(data)
    res.extend(parsed)
print simplejson.dumps(res)
The results however were not ordered by date, as GMP had to use several accounts to twit because Twitter was putting Greather Manchester Police into jail for generating too much traffic. There would be quite a bit to write about that, but let's stick to our work. Here is code to sort the JSON data by time:
#!/usr/bin/python
# Sort the JSON data
# (C) 2010 Enrico Zini <enrico@enricozini.org>
# License: WTFPL version 2 (http://sam.zoy.org/wtfpl/)
import simplejson
import sys
import datetime as dt
all_recs = simplejson.load(sys.stdin)
all_recs.sort(key=lambda x: dt.datetime.strptime(x["created_at"], "%a %b %d %H:%M:%S +0000 %Y"))
simplejson.dump(all_recs, sys.stdout)
I then wanted to play with Tf-idf for extracting the most important words of every tweet:
#!/usr/bin/python
# tfifd - Annotate JSON elements with Tf-idf extracted keywords
#
# Copyright (C) 2010  Enrico Zini <enrico@enricozini.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
import sys, math
import simplejson
import re
# Read all the twits
records = simplejson.load(sys.stdin)
# All the twits by ID
byid = dict(((x["id"], x) for x in records))
# Stopwords we ignore
stopwords = set(["by", "it", "and", "of", "in", "a", "to"])
# Tokenising engine
re_num = re.compile(r"^\d+$")
re_word = re.compile(r"(\w+)")
def tokenise(tweet):
    "Extract tokens from a tweet"
    for tok in tweet["text"].split():
        tok = tok.strip().lower()
        if re_num.match(tok): continue
        mo = re_word.match(tok)
        if not mo: continue
        if mo.group(1) in stopwords: continue
        yield mo.group(1)
# Extract tokens from tweets
tokenised = dict(((x["id"], list(tokenise(x))) for x in records))
# Aggregate token counts
aggregated =  
for d in byid.iterkeys():
    for t in tokenised[d]:
        if t in aggregated:
            aggregated[t] += 1
        else:
            aggregated[t] = 1
def tfidf(doc, tok):
    "Compute TFIDF score of a token in a document"
    return doc.count(tok) * math.log(float(len(byid)) / aggregated[tok])
# Annotate tweets with keywords
res = []
for name, tweet in byid.iteritems():
    doc = tokenised[name]
    keywords = sorted(set(doc), key=lambda tok: tfidf(doc, tok), reverse=True)[:5]
    tweet["keywords"] = keywords
    res.append(tweet)
simplejson.dump(res, sys.stdout)
I thought this was producing a nice summary of every tweet but nobody was particularly interested, so we moved on to adding categories to tweet. Thanks to Yuwei who put together some useful keyword sets, we managed to annotate each tweet with a place name (i.e. "Stockport"), a social place name (i.e. "pub", "bank") and a social category (i.e. "man", "woman", "landlord"...) The code is simple; the biggest work in it was the dictionary of keywords:
#!/usr/bin/python
# categorise - Annotate JSON elements with categories
#
# Copyright (C) 2010  Enrico Zini <enrico@enricozini.org>
# Copyright (C) 2010  Yuwei Lin <yuwei@ylin.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
import sys, math
import simplejson
import re
# Electoral wards from http://en.wikipedia.org/wiki/List_of_electoral_wards_in_Greater_Manchester
placenames = ["Altrincham", "Sale West",
"Altrincham", "Ashton upon Mersey", "Bowdon", "Broadheath", "Hale Barns", "Hale Central", "St Mary", "Timperley", "Village",
"Ashton-under-Lyne",
"Ashton Hurst", "Ashton St Michael", "Ashton Waterloo", "Droylsden East", "Droylsden West", "Failsworth East", "Failsworth West", "St Peter",
"Blackley", "Broughton",
"Broughton", "Charlestown", "Cheetham", "Crumpsall", "Harpurhey", "Higher Blackley", "Kersal",
"Bolton North East",
"Astley Bridge", "Bradshaw", "Breightmet", "Bromley Cross", "Crompton", "Halliwell", "Tonge with the Haulgh",
"Bolton South East",
"Farnworth", "Great Lever", "Harper Green", "Hulton", "Kearsley", "Little Lever", "Darcy Lever", "Rumworth",
"Bolton West",
"Atherton", "Heaton", "Lostock", "Horwich", "Blackrod", "Horwich North East", "Smithills", "Westhoughton North", "Chew Moor", "Westhoughton South",
"Bury North",
"Church", "East", "Elton", "Moorside", "North Manor", "Ramsbottom", "Redvales", "Tottington",
"Bury South",
"Besses", "Holyrood", "Pilkington Park", "Radcliffe East", "Radcliffe North", "Radcliffe West", "St Mary", "Sedgley", "Unsworth",
"Cheadle",
"Bramhall North", "Bramhall South", "Cheadle", "Gatley", "Cheadle Hulme North", "Cheadle Hulme South", "Heald Green", "Stepping Hill",
"Denton", "Reddish",
"Audenshaw", "Denton North East", "Denton South", "Denton West", "Dukinfield", "Reddish North", "Reddish South",
"Hazel Grove",
"Bredbury", "Woodley", "Bredbury Green", "Romiley", "Hazel Grove", "Marple North", "Marple South", "Offerton",
"Heywood", "Middleton",
"Bamford", "Castleton", "East Middleton", "Hopwood Hall", "Norden", "North Heywood", "North Middleton", "South Middleton", "West Heywood", "West Middleton",
"Leigh",
"Astley Mosley Common", "Atherleigh", "Golborne", "Lowton West", "Leigh East", "Leigh South", "Leigh West", "Lowton East", "Tyldesley",
"Makerfield",
"Abram", "Ashton", "Bryn", "Hindley", "Hindley Green", "Orrell", "Winstanley", "Worsley Mesnes",
"Manchester Central",
"Ancoats", "Clayton", "Ardwick", "Bradford", "City Centre", "Hulme", "Miles Platting", "Newton Heath", "Moss Side", "Moston",
"Manchester", "Gorton",
"Fallowfield", "Gorton North", "Gorton South", "Levenshulme", "Longsight", "Rusholme", "Whalley Range",
"Manchester", "Withington",
"Burnage", "Chorlton", "Chorlton Park", "Didsbury East", "Didsbury West", "Old Moat", "Withington",
"Oldham East", "Saddleworth",
"Alexandra", "Crompton", "Saddleworth North", "Saddleworth South", "Saddleworth West", "Lees", "St James", "St Mary", "Shaw", "Waterhead",
"Oldham West", "Royton",
"Chadderton Central", "Chadderton North", "Chadderton South", "Coldhurst", "Hollinwood", "Medlock Vale", "Royton North", "Royton South", "Werneth",
"Rochdale",
"Balderstone", "Kirkholt", "Central Rochdale", "Healey", "Kingsway", "Littleborough Lakeside", "Milkstone", "Deeplish", "Milnrow", "Newhey", "Smallbridge", "Firgrove", "Spotland", "Falinge", "Wardle", "West Littleborough",
"Salford", "Eccles",
"Claremont", "Eccles", "Irwell Riverside", "Langworthy", "Ordsall", "Pendlebury", "Swinton North", "Swinton South", "Weaste", "Seedley",
"Stalybridge", "Hyde",
"Dukinfield Stalybridge", "Hyde Godley", "Hyde Newton", "Hyde Werneth", "Longdendale", "Mossley", "Stalybridge North", "Stalybridge South",
"Stockport",
"Brinnington", "Central", "Davenport", "Cale Green", "Edgeley", "Cheadle Heath", "Heatons North", "Heatons South", "Manor",
"Stretford", "Urmston",
"Bucklow-St Martins", "Clifford", "Davyhulme East", "Davyhulme West", "Flixton", "Gorse Hill", "Longford", "Stretford", "Urmston",
"Wigan",
"Aspull New Springs Whelley", "Douglas", "Ince", "Pemberton", "Shevington with Lower Ground", "Standish with Langtree", "Wigan Central", "Wigan West",
"Worsley", "Eccles South",
"Barton", "Boothstown", "Ellenbrook", "Cadishead", "Irlam", "Little Hulton", "Walkden North", "Walkden South", "Winton", "Worsley",
"Wythenshawe", "Sale East",
"Baguley", "Brooklands", "Northenden", "Priory", "Sale Moor", "Sharston", "Woodhouse Park"]
# Manual coding from Yuwei
placenames.extend(["City centre", "Tameside", "Oldham", "Bury", "Bolton",
"Trafford", "Pendleton", "New Moston", "Denton", "Eccles", "Leigh", "Benchill",
"Prestwich", "Sale", "Kearsley", ])
placenames.extend(["Trafford", "Bolton", "Stockport", "Levenshulme", "Gorton",
"Tameside", "Blackley", "City centre", "Airport", "South Manchester",
"Rochdale", "Chorlton", "Uppermill", "Castleton", "Stalybridge", "Ashton",
"Chadderton", "Bury", "Ancoats", "Whalley Range", "West Yorkshire",
"Fallowfield", "New Moston", "Denton", "Stretford", "Eccles", "Pendleton",
"Leigh", "Altrincham", "Sale", "Prestwich", "Kearsley", "Hulme", "Withington",
"Moss Side", "Milnrow", "outskirt of Manchester City Centre", "Newton Heath",
"Wythenshawe", "Mancunian Way", "M60", "A6", "Droylesden", "M56", "Timperley",
"Higher Ince", "Clayton", "Higher Blackley", "Lowton", "Droylsden",
"Partington", "Cheetham Hill", "Benchill", "Longsight", "Didsbury",
"Westhoughton"])
# Social categories from Yuwei
soccat = ["man", "woman", "men", "women", "youth", "teenager", "elderly",
"patient", "taxi driver", "neighbour", "male", "tenant", "landlord", "child",
"children", "immigrant", "female", "workmen", "boy", "girl", "foster parents",
"next of kin"]
for i in range(100):
    soccat.append("%d-year-old" % i)
    soccat.append("%d-years-old" % i)
# Types of social locations from Yuwei
socloc = ["car park", "park", "pub", "club", "shop", "premises", "bus stop",
"property", "credit card", "supermarket", "garden", "phone box", "theatre",
"toilet", "building site", "Crown court", "hard shoulder", "telephone kiosk",
"hotel", "restaurant", "cafe", "petrol station", "bank", "school",
"university"]
extras =   "placename": placenames, "soccat": soccat, "socloc": socloc  
# Normalise keyword lists
for k, v in extras.iteritems():
    # Remove duplicates
    v = list(set(v))
    # Sort by length
    v.sort(key=lambda x:len(x), reverse=True)
# Add keywords
def add_categories(tweet):
    text = tweet["text"].lower()
    for field, categories in extras.iteritems():
        for cat in categories:
            if cat.lower() in text:
                tweet[field] = cat
                break
    return tweet
# Read all the twits
records = (add_categories(x) for x in simplejson.load(sys.stdin))
simplejson.dump(list(records), sys.stdout)
All these scripts form a nice processing chain: each script takes a list of JSON records, adds some bit and passes it on. In order to see what we have so far, here is a simple script to convert the JSON twits to CSV so they can be viewed in a spreadsheet:
#!/usr/bin/python
# Convert the JSON twits to CSV
# (C) 2010 Enrico Zini <enrico@enricozini.org>
# License: WTFPL version 2 (http://sam.zoy.org/wtfpl/)
import simplejson
import sys
import csv
rows = ["id", "created_at", "text", "keywords", "placename"]
writer = csv.writer(sys.stdout)
for rec in simplejson.load(sys.stdin):
    rec["keywords"] = " ".join(rec["keywords"])
    rec["placename"] = rec.get("placename", "")
    writer.writerow([rec[row] for row in rows])
At this point we were coming up with lots of questions: "were there more reports on women or men?", "which place had most incidents?", "what were the incidents involving animals?"... Time to bring Xapian into play. This script reads all the JSON tweets and builds a Xapian index with them:
#!/usr/bin/python
# toxapian - Index JSON tweets in Xapian
#
# Copyright (C) 2010  Enrico Zini <enrico@enricozini.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
import simplejson
import sys
import os, os.path
import xapian
DBNAME = sys.argv[1]
db = xapian.WritableDatabase(DBNAME, xapian.DB_CREATE_OR_OPEN)
stemmer = xapian.Stem("english")
indexer = xapian.TermGenerator()
indexer.set_stemmer(stemmer)
indexer.set_database(db)
data = simplejson.load(sys.stdin)
for rec in data:
    doc = xapian.Document()
    doc.set_data(str(rec["id"]))
    indexer.set_document(doc)
    indexer.index_text_without_positions(rec["text"])
    # Index categories as categories
    if "placename" in rec:
        doc.add_boolean_term("XP" + rec["placename"].lower())
    if "soccat" in rec:
        doc.add_boolean_term("XS" + rec["soccat"].lower())
    if "socloc" in rec:
        doc.add_boolean_term("XL" + rec["socloc"].lower())
    db.add_document(doc)
db.flush()
# Also save the whole dataset so we know where to find it later if we want to
# show the details of an entry
simplejson.dump(data, open(os.path.join(DBNAME, "all.json"), "w"))
And this is a simple command line tool to query to the database:
#!/usr/bin/python
# xgrep - Command line tool to query the GMP24 tweet Xapian database
#
# Copyright (C) 2010  Enrico Zini <enrico@enricozini.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
import simplejson
import sys
import os, os.path
import xapian
DBNAME = sys.argv[1]
db = xapian.Database(DBNAME)
stem = xapian.Stem("english")
qp = xapian.QueryParser()
qp.set_default_op(xapian.Query.OP_AND)
qp.set_database(db)
qp.set_stemmer(stem)
qp.set_stemming_strategy(xapian.QueryParser.STEM_SOME)
qp.add_boolean_prefix("place", "XP")
qp.add_boolean_prefix("soc", "XS")
qp.add_boolean_prefix("loc", "XL")
query = qp.parse_query(sys.argv[2],
    xapian.QueryParser.FLAG_BOOLEAN  
    xapian.QueryParser.FLAG_LOVEHATE  
    xapian.QueryParser.FLAG_BOOLEAN_ANY_CASE  
    xapian.QueryParser.FLAG_WILDCARD  
    xapian.QueryParser.FLAG_PURE_NOT  
    xapian.QueryParser.FLAG_SPELLING_CORRECTION  
    xapian.QueryParser.FLAG_AUTO_SYNONYMS)
enquire = xapian.Enquire(db)
enquire.set_query(query)
count = 40
matches = enquire.get_mset(0, count)
estimated = matches.get_matches_estimated()
print "%d/%d results" % (matches.size(), estimated)
data = dict((str(x["id"]), x) for x in simplejson.load(open(os.path.join(DBNAME, "all.json"))))
for m in matches:
    rec = data[m.document.get_data()]
    print rec["text"]
print "%d/%d results" % (matches.size(), matches.get_matches_estimated())
total = db.get_doccount()
estimated = matches.get_matches_estimated()
print "%d results over %d documents, %d%%" % (estimated, total, estimated * 100 / total)
Neat! Now that we have a proper index that supports all sort of cool things, like stemming, tag clouds, full text search with complex queries, lookup of similar documents, suggest keywords and so on, it was just fair to put together a web service to share it with other people at the event. It helped that I had already written similar code for apt-xapian-index and dde before. Here is the server, quickly built on bottle. The very last line starts the server and it is where you can configure the listening interface and port.
#!/usr/bin/python
# xserve - Make the GMP24 tweet Xapian database available on the web
#
# Copyright (C) 2010  Enrico Zini <enrico@enricozini.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
import bottle
from bottle import route, post
from cStringIO import StringIO
import cPickle as pickle
import simplejson
import sys
import os, os.path
import xapian
import urllib
import math
bottle.debug(True)
DBNAME = sys.argv[1]
QUERYLOG = os.path.join(DBNAME, "queries.txt")
data = dict((str(x["id"]), x) for x in simplejson.load(open(os.path.join(DBNAME, "all.json"))))
prefixes =   "place": "XP", "soc": "XS", "loc": "XL"  
prefix_desc =   "place": "Place name", "soc": "Social category", "loc": "Social location"  
db = xapian.Database(DBNAME)
stem = xapian.Stem("english")
qp = xapian.QueryParser()
qp.set_default_op(xapian.Query.OP_AND)
qp.set_database(db)
qp.set_stemmer(stem)
qp.set_stemming_strategy(xapian.QueryParser.STEM_SOME)
for k, v in prefixes.iteritems():
    qp.add_boolean_prefix(k, v)
def make_query(qstring):
    return qp.parse_query(qstring,
        xapian.QueryParser.FLAG_BOOLEAN  
        xapian.QueryParser.FLAG_LOVEHATE  
        xapian.QueryParser.FLAG_BOOLEAN_ANY_CASE  
        xapian.QueryParser.FLAG_WILDCARD  
        xapian.QueryParser.FLAG_PURE_NOT  
        xapian.QueryParser.FLAG_SPELLING_CORRECTION  
        xapian.QueryParser.FLAG_AUTO_SYNONYMS)
@route("/")
def index():
    query = urllib.unquote_plus(bottle.request.GET.get("q", ""))
    out = StringIO()
    print >>out, '''
<html>
<head>
<title>Query</title>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
<script type="text/javascript">
$(function() 
    $("#queryfield")[0].focus()
 )
</script>
</head>
<body>
<h1>Search</h1>
<form method="POST" action="/query">
Keywords: <input type="text" name="query" value="%s" id="queryfield">
<input type="submit">
<a href="http://xapian.org/docs/queryparser.html">Help</a>
</form>''' % query
    print >>out, '''
<p>Example: "car place:wigan"</p>

<p>Available prefixes:</p>

<ul>
'''
    for pfx in prefixes.keys():
        print >>out, "<li><a href='/catinfo/%s'>%s - %s</a></li>" % (pfx, pfx, prefix_desc[pfx])
    print >>out, '''
</ul>
'''
    oldqueries = []
    if os.path.exists(QUERYLOG):
        total = db.get_doccount()
        fd = open(QUERYLOG, "r")
        while True:
            try:
                q = pickle.load(fd)
            except EOFError:
                break
            oldqueries.append(q)
        fd.close()
        def print_query(q):
            count = q["count"]
            print >>out, "<li><a href='/query?query=%s'>%s (%d/%d %.2f%%)</a></li>" % (urllib.quote_plus(q["q"]), q["q"], count, total, count * 100.0 / total)
        print >>out, "<p>Last 10 queries:</p><ul>"
        for q in oldqueries[:-10:-1]:
            print_query(q)
        print >>out, "</ul>"
        # Remove duplicates
        oldqueries = dict(((x["q"], x) for x in oldqueries)).values()
        print >>out, "<table>"
        print >>out, "<tr><th>10 queries with most results</th><th>10 queries with least results</th></tr>"
        print >>out, "<tr><td>"
        print >>out, "<ul>"
        oldqueries.sort(key=lambda x:x["count"], reverse=True)
        for q in oldqueries[:10]:
            print_query(q)
        print >>out, "</ul>"
        print >>out, "</td><td>"
        print >>out, "<ul>"
        nonempty = [x for x in oldqueries if x["count"] > 0]
        nonempty.sort(key=lambda x:x["count"])
        for q in nonempty[:10]:
            print_query(q)
        print >>out, "</ul>"
        print >>out, "</td></tr>"
        print >>out, "</table>"
    print >>out, '''
</body>
</html>'''
    return out.getvalue()
@route("/query")
@route("/query/")
@post("/query")
@post("/query/")
def query():
    query = bottle.request.POST.get("query", bottle.request.GET.get("query", ""))
    enquire = xapian.Enquire(db)
    enquire.set_query(make_query(query))
    count = 40
    matches = enquire.get_mset(0, count)
    estimated = matches.get_matches_estimated()
    total = db.get_doccount()
    out = StringIO()
    print >>out, '''
<html>
<head><title>Results</title></head>
<body>
<h1>Results for "<b>%s</b>"</h1>
''' % query
    if estimated == 0:
        print >>out, "No results found."
    else:
        # Give as results the first 30 documents; also use them as the key
        # ones to use to compute relevant terms
        rset = xapian.RSet()
        for m in enquire.get_mset(0, 30):
            rset.add_document(m.document.get_docid())
        # Compute the tag cloud
        class NonTagFilter(xapian.ExpandDecider):
            def __call__(self, term):
                return not term[0].isupper() and not term[0].isdigit()
        cloud = []
        maxscore = None
        for res in enquire.get_eset(40, rset, NonTagFilter()):
            # Normalise the score in the interval [0, 1]
            weight = math.log(res.weight)
            if maxscore == None: maxscore = weight
            tag = res.term
            cloud.append([tag, float(weight) / maxscore])
        max_weight = cloud[0][1]
        min_weight = cloud[-1][1]
        cloud.sort(key=lambda x:x[0])
        def mklink(query, term):
            return "/query?query=%s" % urllib.quote_plus(query + " and " + term)
        print >>out, "<h2>Tag cloud</h2>"
        print >>out, "<blockquote>"
        for term, weight in cloud:
            size = 100 + 100.0 * (weight - min_weight) / (max_weight - min_weight)
            print >>out, "<a href='%s' style='font-size:%d%%; color:brown;'>%s</a>" % (mklink(query, term), size, term)
        print >>out, "</blockquote>"
        print >>out, "<h2>Results</h2>"
        print >>out, "<p><a href='/'>Search again</a></p>"
        print >>out, "<p>%d results over %d documents, %.2f%%</p>" % (estimated, total, estimated * 100.0 / total)
        print >>out, "<p>%d/%d results</p>" % (matches.size(), estimated)
        print >>out, "<ul>"
        for m in matches:
            rec = data[m.document.get_data()]
            print >>out, "<li><a href='/item/%s'>%s</a></li>" % (rec["id"], rec["text"])
        print >>out, "</ul>"
        fd = open(QUERYLOG, "a")
        qinfo = dict(q=query, count=estimated)
        pickle.dump(qinfo, fd)
        fd.close()
    print >>out, '''
<a href="/">Search again</a>

</body>
</html>'''
    return out.getvalue()
@route("/item/:id")
@route("/item/:id/")
def show(id):
    rec = data[id]
    out = StringIO()
    print >>out, '''
<html>
<head><title>Result %s</title></head>
<body>
<h1>Raw JSON record for twit %s</h1>
<pre>''' % (rec["id"], rec["id"])
    print >>out, simplejson.dumps(rec, indent=" ")
    print >>out, '''
</pre>
</body>
</html>'''
    return out.getvalue()
@route("/catinfo/:name")
@route("/catinfo/:name/")
def catinfo(name):
    prefix = prefixes[name]
    out = StringIO()
    print >>out, '''
<html>
<head><title>Values for %s</title></head>
<body>
''' % name
    terms = [(x.term[len(prefix):], db.get_termfreq(x.term)) for x in db.allterms(prefix)]
    terms.sort(key=lambda x:x[1], reverse=True)
    freq_min = terms[0][1]
    freq_max = terms[-1][1]
    def mklink(name, term):
        return "/query?query=%s" % urllib.quote_plus(name + ":" + term)
    # Build tag cloud
    print >>out, "<h1>Tag cloud</h1>"
    print >>out, "<blockquote>"
    for term, freq in sorted(terms[:20], key=lambda x:x[0]):
        size = 100 + 100.0 * (freq - freq_min) / (freq_max - freq_min)
        print >>out, "<a href='%s' style='font-size:%d%%; color:brown;'>%s</a>" % (mklink(name, term), size, term)
    print >>out, "</blockquote>"
    print >>out, "<h1>All terms</h1>"
    print >>out, "<table>"
    print >>out, "<tr><th>Occurrences</th><th>Name</th></tr>"
    for term, freq in terms:
        print >>out, "<tr><td>%d</td><td><a href='/query?query=%s'>%s</a></td></tr>" % (freq, urllib.quote_plus(name + ":" + term), term)
    print >>out, "</table>"
    print >>out, '''
</body>
</html>'''
    return out.getvalue()
# Change here for bind host and port
bottle.run(host="0.0.0.0", port=8024)
...and then we presented our work and ended up winning the contest. This was the story of how we wrote this set of award winning code.

Alexander Reichle-Schmehl: Release Critical Bug report for Week 41

The bug webinterface of the Universal Debian Database currently knows about the following release critical bugs:
In Total:732
Affecting Squeeze:327
Squeeze only:84
Remaining to be fixed in Squeeze:243
Of these 243 bugs, the following tags are set:
Pending in Squeeze:22
Patched in Squeeze:51
Duplicates in Squeeze:25
Can be fixed in a security Update:26
Contrib or non-free in Squeeze:12
Otherwise fixed in Squeeze:26
Ignoring all the above (multiple tags possible), 130 bugs need to be fixed by Debian Contributors to get Debian 6.0 Squeeze released. However, with the view of the Release Managers, 183 need to be dealt with for the release to happen. Please see Interpreting the release critical bug statistics for an explanation of the different numbers.

14 October 2010

Rapha&#235;l Hertzog: Correctly renaming a conffile in Debian package maintainer scripts

After having dealt with the removal of obsolete conffiles, I ll now explain what you should do when a configuration file managed by dpkg must be renamed. The problem Let s suppose that version 1.2 of the software stopped providing /etc/foo.conf. Instead it provides /etc/bar.conf because the configuration file got renamed. If you do nothing special, the new conffile will be installed with the default configuration, and the old one will stay around. Any customization made by the administrator are lost in the process (in fact they are not lost, they are still in foo.conf but they are unused). Of course, you could do mv /etc/foo.conf /etc/bar.conf in the pre-installation script. But that s not satisfactory: it will generate a spurious conffile prompt that the end-user will not understand. The solution In the preinst script, you have to verify if the old conffile has been modified by the administrator. If yes, you want to keep the file around. Otherwise you know you will be able to ditch it once the upgrade is over, and you rename it to /etc/foo.conf.dpkg-remove to remember this fact. In the postinst script, you remove /etc/foo.conf.dpkg-remove. If the old conffile (/etc/foo.conf) still exists, it s because it was modified by the administrator. You make a backup of the new conffile in /etc/bar.conf.dpkg-dist and rename the old one into /etc/bar.conf. In the postrm, when called to abort an upgrade, you move /etc/foo.conf.dpkg-remove back to its original name. In practice, use dpkg-maintscript-helper dpkg-maintscript-helper can automate all those tasks. You just have to put the following snippet in the maintainer scripts (postinst, postrm, preinst):
if dpkg-maintscript-helper supports mv_conffile 2>/dev/null; then
    dpkg-maintscript-helper mv_conffile /etc/foo.conf /etc/bar.conf 1.1-3 -- "$@"
fi
In this example, I assumed that version 1.1-3 was the last version of the package that contained /etc/foo.conf (i.e. the last version released before 1.2-1 was packaged). You can avoid the preliminary test if you pre-depend on dpkg (>= 1.15.7.2) or if enough time has passed to assume that everybody has a newer version anyway. You can learn all the details in dpkg-maintscript-helper s manual page. Found it useful? Be sure to not miss other packaging tips (or lessons), click here to subscribe to my free newsletter and get new articles by email (just check Send me blog updates ).
Flattr this Share/Bookmark No comment Support my work

MJ Ray: UCEPROTECT and SORBS SPAM: Two DNSBLs to Avoid

I just had another conflict with a debatable anti-spam configuration. I think it was debatable rather than obviously wrong, so I m not going to rant about the specific setup, but while discussing it with other postmasters, the following two Domain Name Service Blacklist (DNSBL) facts surprised me:
  1. UCEPROTECT3 can be evaded if you pay them EUR 75 every 2 years. UCEPROTECT is a German organisation but refuses to publish the details usually found on an Impressum so you could be funding spammers, for all you know. Their four suggestions to stop spam (no C-R, rate limits, block port 25, refuse to sell) seem so unlikely to stop spam it s either na ve or insincere.
  2. SORBS asks ISPs to pay USD 50 to get delisted from the SORBS SPAM database. It s to charity rather than SORBS, but it still seems like a protection racket. Spammers can get out of jail for a few dollars?
Those lists both seem quite trigger-happy to me, which isn t surprising as these payment demands encourage listing first and asking questions later. It means I won t be using these two lists anywhere soon.

13 October 2010

Kees Cook: mountall umask

The recent CVE-2010-2961 mountall vulnerability got a nice write-up by xorl today. I ve seen a few public exploits for it, but those that I ve seen, including the one in xorl s post, miss a rather important point: udev events can be triggered by regular users without any hardware fiddling. While the bug that kept udev from running inotify correctly on the /dev/.udev/rules.d directory during initial boot kept this vulnerability exposure pretty well minimized, the fact that udev events can be triggered at will made it pretty bad too. If udev had already been restarted, an attacker didn t have to wait at all, nor have physical access to the system. While it is generally understood that udev events are related to hardware, it s important to keep in mind that it also sends events on module loads, and module loads can happen on demand from unprivileged users. For example, say you want to send an X.25 packet, when you call socket(AF_X25, SOCK_STREAM), the kernel will go load net-pf-9, which modules.alias lists as the x25 module. And once loaded, udev sends a module event. (Which, by the way, should serve as a reminder to people to block module loading if you can.) So, as I mentioned, here s yet another exploit for the mountall vulnerability: mountall-CVE-2010-2961.py. It writes to the vulnerable udev rule file and then attempts to trigger udev immediately by walking a list of possible socket() AF_* types.

12 October 2010

Martin Zobel-Helas: Congratulation go to KiBi

#600000

[Update: and to Rene Mayorga, for winning the 600000thBugContest!]

11 October 2010

Dirk Eddelbuettel: Chicago Marathon 2010

It's the Monday of the Columbus Day weekend here, so I must have been running a Chicago Marathon yesterday. Indeed -- the 34th annual Chicago Marathon took place yesterday but everything was about its 10/10/10 date. The symmetric set of numbers was in all advertisements, posters, street signs, on our bibs, the medals, the (nice, for once) race shirt. Everywhere, and yes, I will admit that it was also part of the reason I finally registered earlier in the year shortly before the race sold out. This was the sixth time I ran this race (and my 14th marathon overall). And I still can't run this course all that well: never got a Boston qualification here. As I had mentioned when I blogged about my third Boston Marathon earlier in the year and the recent Chicago Half-Marathon, I have had some recurrent issue with a sore achilles which limited my running throughout the year. It had gotten better but a quick summary of the miles in my running log showed that I had been running only about 80% of the training miles I had in prior years. And not a single 20-miler. I knew I'd have to pay for that. Plus, as so often, the weather. Not quite as hot as the record-heat of 2007. But close enough: high 60s at the start and high 70s or even low 80s towards the end. But I have to compliment to the race organisers. The race was very well organised (following the experience of 2007) with extra water stops, extra sponges handed out at several spots (!!) and very good communication when during the race the alert level was raised to yellow given the heat and humidity. The searchable results now show a fair number of non-finishers, but at least nobody seems to have died. But it looked ugly on the course. I think I ran by three or four sets of paramedics assisting runners who were 'down and out'.. So how did I do? Fair, I suppose -- I ran pretty well for sixteen miles, then needed a first short walking break and continued to run well towards and past the 18 mile waterstop where a bunch of friends and fellow Oak Park runners were helping. But not long after that, I crumbled and needed to alternate walking and running for most of the remainder. With that I came in at 3:41:41, or a 8:28 min/mile pace. And which is by two seconds slower than the previous 'worst' from 2007. But heck, at least it's still more than three minutes faster than Dubya in Houston in 1993 ... I also got beat by a few local running friends as well as by Chicago's own marathon juggler. So there. Maybe I'll train a bit more next time.

Next.

Previous.